manifestLoadingTimeOut: 20000,
manifestLoadingMaxRetry: 4,
levelLoadingTimeOut: 20000,
fragLoadingTimeOut: 20000
});
let settled = false;
_wsHls.on(Hls.Events.MANIFEST_PARSED, () => {
if (settled) return;
settled = true;
els.audio.play().then(resolve).catch(reject);
});
_wsHls.on(Hls.Events.ERROR, (_, data) => {
if (data && data.fatal) {
if (!settled) { settled = true; reject(new Error(data.details || 'hls_error')); }
this.onError();
}
});
_wsHls.loadSource(url);
_wsHls.attachMedia(els.audio);
});
}
} else {
els.audio.src = url + (url.includes('?') ? '&' : '?') + '_t=' + Date.now();
await els.audio.play();
}
} catch (e) {
this.setLoading(false);
if (!this.isStopping) {
utils.toast('Erro ao reproduzir', 'error');
}
}
},
stop() {
this.isStopping = true;
this.setLoading(false);
els.audio.pause();
wsDetachHls();
els.audio.src = '';
this.onPause();
},
onPlay() {
state.isPlaying = true;
this.setLoading(false);
els.playBtn.innerHTML = ' ';
els.playBtn.classList.add('playing');
els.miniPlayBtn.innerHTML = ' ';
els.miniPlayBtn.classList.add('playing');
els.visualizer.classList.add('active');
// Só mostrar mini player se não foi fechado manualmente
if (!state.miniPlayerClosed) {
els.miniPlayer.classList.add('visible');
}
els.playerCard?.classList.add('playing');
this.animateVisualizer();
// Ativar particulas agitadas
if (typeof particles !== 'undefined') {
particles.setPlaying(true);
}
},
onPause() {
state.isPlaying = false;
this.setLoading(false);
els.playBtn.innerHTML = ' ';
els.playBtn.classList.remove('playing');
els.miniPlayBtn.innerHTML = ' ';
els.miniPlayBtn.classList.remove('playing');
els.visualizer.classList.remove('active');
els.playerCard?.classList.remove('playing');
// Desativar particulas agitadas
if (typeof particles !== 'undefined') {
particles.setPlaying(false);
}
},
onError() {
this.setLoading(false);
// So mostra erro se nao estiver parando intencionalmente
if (!this.isStopping && state.isPlaying) {
utils.toast('Erro de conexão. Reconectando...', 'error');
setTimeout(() => { if (state.isPlaying) this.play(); }, 3000);
}
},
setVolume(val) {
state.volume = parseInt(val);
els.audio.volume = state.volume / 100;
els.volumeSlider.value = state.volume;
els.miniVolumeSlider.value = state.volume;
els.volumeValue.textContent = state.volume + '%';
const icon = state.volume === 0 ? 'volume-mute' : state.volume < 50 ? 'volume-down' : 'volume-up';
els.volumeIcon.className = `fas fa-${icon}`;
},
toggleMute() {
this.setVolume(state.volume > 0 ? 0 : 80);
},
animateVisualizer() {
const bars = els.visualizer.querySelectorAll('span');
const animate = () => {
if (!state.isPlaying) return;
bars.forEach(bar => {
bar.style.height = Math.random() * 80 + 20 + '%';
});
setTimeout(animate, 100);
};
animate();
},
lastSongTitle: '',
currentMusicId: null,
minListeners: 5, // Minimo de ouvintes reais para mostrar
simulatedListeners: 0, // Contador de ouvintes simulados
lastSimulatedUpdate: 0, // Timestamp da ultima atualizacao simulada
async updateNowPlaying() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/get_player_nowplaying.php?hash=${CONFIG.hash}&direct_art=1`);
const data = await res.json();
if (data.success && data.data) {
const song = data.data.song || {};
const title = song.title || CONFIG.radioName;
const artist = song.artist || '';
// Pegar arte da estrutura correta
const artData = data.data.art || {};
const art = artData.url_direct || artData.url || '';
// Pegar progresso da musica (AzuraCast)
const elapsed = data.data.elapsed || 0;
const duration = data.data.duration || song.duration || 0;
const playedAt = data.data.played_at || 0;
// Valores de exibição (respeitam toggle exibirMusica)
const displayTitle = CONFIG.exibirMusica ? title : CONFIG.radioName;
const displayArtist = CONFIG.exibirMusica ? (artist || '') : 'Ao Vivo';
const displayArt = CONFIG.exibirMusica ? art : (CONFIG.logoUrl || '');
// Detectar mudança de música para atualizar histórico
const currentSong = `${title}-${artist}`;
const songChanged = this.lastSongTitle && this.lastSongTitle !== currentSong;
// Usar song.id do AzuraCast como music_id (com fallback para hash de título+artista)
const musicId = song.id || (title && artist ? btoa(unescape(encodeURIComponent(`${title}|${artist}`))).replace(/[^a-zA-Z0-9]/g, '').substring(0, 32) : null);
if (songChanged) {
// Música mudou - aplicar transição e resetar progresso
this.animateSongChange(displayTitle, displayArtist, displayArt);
setTimeout(() => history.load(), 2000);
// Atualizar music_id e carregar votos
this.currentMusicId = musicId;
this.loadVotes();
// Resetar progresso
state.songDuration = duration;
state.songStartTime = playedAt > 0 ? playedAt : (Date.now() / 1000);
this.updateProgressRing(0);
} else if (!this.lastSongTitle) {
// Primeira carga - sem animação
els.songTitle.textContent = displayTitle;
els.miniTitle.textContent = displayTitle;
els.songArtist.textContent = displayArtist || CONFIG.radioName;
els.miniArtist.textContent = displayArtist || CONFIG.radioName;
if (displayArt) {
els.coverArt.src = displayArt;
els.miniCover.src = displayArt;
}
// Inicializar music_id e carregar votos
this.currentMusicId = musicId;
this.loadVotes();
// Inicializar progresso
state.songDuration = duration;
state.songStartTime = playedAt > 0 ? playedAt : (Date.now() / 1000 - elapsed);
if (duration > 0) {
this.updateProgressRing(elapsed / duration);
}
} else {
// Mesma musica - atualizar progresso se nao temos start time
if (duration > 0 && state.songDuration !== duration) {
state.songDuration = duration;
state.songStartTime = playedAt > 0 ? playedAt : (Date.now() / 1000 - elapsed);
}
}
this.lastSongTitle = currentSong;
// Listeners - exibir conforme configuracao
const listenersEl = $('#listeners');
const listenersSpan = $('#listeners span');
if (!CONFIG.exibirOuvintes) {
// Exibir ouvintes desligado
listenersEl.classList.add('hidden');
} else if (CONFIG.ouvintesSimulados) {
// Ouvintes simulados - atualizar a cada 30-60 segundos
const now = Date.now();
if (now - this.lastSimulatedUpdate > 30000 + Math.random() * 30000 || this.simulatedListeners === 0) {
// Gerar valor aleatorio entre 15 e 85
this.simulatedListeners = Math.floor(Math.random() * (85 - 15 + 1)) + 15;
this.lastSimulatedUpdate = now;
}
listenersSpan.textContent = this.simulatedListeners;
listenersEl.classList.remove('hidden');
} else {
// Ouvintes reais - so mostrar a partir de 5
const listenersData = data.data.listeners || {};
const listenerCount = listenersData.current || listenersData.unique || 0;
if (listenerCount >= this.minListeners) {
listenersSpan.textContent = listenerCount;
listenersEl.classList.remove('hidden');
} else {
listenersEl.classList.add('hidden');
}
}
}
} catch (e) {
console.error('[Player] Erro ao atualizar:', e);
// Se e a primeira carga e falhou, mostrar nome da radio como fallback
if (!this.lastSongTitle) {
if (els.songTitle) els.songTitle.textContent = CONFIG.radioName;
if (els.miniTitle) els.miniTitle.textContent = CONFIG.radioName;
if (els.songArtist) els.songArtist.textContent = 'Conectando...';
if (els.miniArtist) els.miniArtist.textContent = 'Conectando...';
}
}
},
animateSongChange(title, artist, art) {
// === FASE 1: Fade out de todos os elementos ===
els.songTitle.classList.add('changing');
els.songArtist.classList.add('changing');
els.miniTitle.classList.add('changing');
els.miniArtist.classList.add('changing');
if (art) {
els.coverArt.classList.add('cover-out');
els.miniCover.classList.add('changing');
}
// === FASE 2: Aguardar fade out completar (500ms) ===
setTimeout(() => {
// Atualizar textos
els.songTitle.textContent = title;
els.miniTitle.textContent = title;
els.songArtist.textContent = artist || CONFIG.radioName;
els.miniArtist.textContent = artist || CONFIG.radioName;
// Fade in dos textos
requestAnimationFrame(() => {
els.songTitle.classList.remove('changing');
els.songArtist.classList.remove('changing');
els.miniTitle.classList.remove('changing');
els.miniArtist.classList.remove('changing');
});
// Atualizar capa com preload
if (art) {
const preload = new Image();
const fadeInCover = () => {
els.coverArt.src = art;
els.miniCover.src = art;
// Trocar de cover-out para cover-in (posição de entrada)
els.coverArt.classList.remove('cover-out');
els.coverArt.classList.add('cover-in');
els.miniCover.classList.remove('changing');
// Próximo frame: remover cover-in para animar entrada
requestAnimationFrame(() => {
requestAnimationFrame(() => {
els.coverArt.classList.remove('cover-in');
});
});
};
preload.onload = fadeInCover;
preload.onerror = fadeInCover;
preload.src = art;
}
}, 500);
},
// Carregar votos da música atual
async loadVotes() {
if (!this.currentMusicId) return;
try {
// Pegar título e artista atuais para salvar junto com o voto
const title = els.songTitle ? els.songTitle.textContent : '';
const artist = els.songArtist ? els.songArtist.textContent : '';
const res = await fetch(`${CONFIG.apiBase}/vote_music.php?hash=${CONFIG.hash}&music_id=${encodeURIComponent(this.currentMusicId)}`);
const data = await res.json();
if (data.success) {
// Atualizar contadores
if (els.likeCount) els.likeCount.textContent = data.likes || 0;
if (els.dislikeCount) els.dislikeCount.textContent = data.dislikes || 0;
// Atualizar estado dos botões
const btnLike = $('#btn-like');
const btnDislike = $('#btn-dislike');
const miniLike = $('#mini-like');
const miniDislike = $('#mini-dislike');
// Remover classes active
[btnLike, btnDislike, miniLike, miniDislike].forEach(btn => {
if (btn) btn.classList.remove('active');
});
// Aplicar classe active no voto do usuário
if (data.user_vote === 1) {
if (btnLike) btnLike.classList.add('active');
if (miniLike) miniLike.classList.add('active');
} else if (data.user_vote === -1) {
if (btnDislike) btnDislike.classList.add('active');
if (miniDislike) miniDislike.classList.add('active');
}
}
} catch (e) {
console.error('Erro ao carregar votos:', e);
}
},
triggerVoteEffect(btn, type) {
if (!btn) return;
const color = type === 'like' ? '#22c55e' : '#ef4444';
const colorAlt = type === 'like' ? '#4ade80' : '#f87171';
// Bounce the icon
btn.classList.remove('vote-animating');
void btn.offsetWidth; // force reflow
btn.classList.add('vote-animating');
// Ring burst
const ring = document.createElement('div');
ring.className = 'vote-ring';
ring.style.borderColor = color;
btn.appendChild(ring);
// Particles
const count = 10;
for (let i = 0; i < count; i++) {
const p = document.createElement('div');
p.className = 'vote-particle';
const angle = (i / count) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
const dist = 20 + Math.random() * 20;
p.style.setProperty('--vp-x', `${Math.cos(angle) * dist}px`);
p.style.setProperty('--vp-y', `${Math.sin(angle) * dist}px`);
p.style.left = '50%';
p.style.top = '50%';
const size = 3 + Math.random() * 4;
p.style.width = size + 'px';
p.style.height = size + 'px';
p.style.marginLeft = -(size / 2) + 'px';
p.style.marginTop = -(size / 2) + 'px';
p.style.background = Math.random() > 0.5 ? color : colorAlt;
p.style.animationDelay = (Math.random() * 0.08) + 's';
btn.appendChild(p);
}
// Cleanup
setTimeout(() => {
btn.classList.remove('vote-animating');
btn.querySelectorAll('.vote-particle, .vote-ring').forEach(el => el.remove());
}, 700);
},
async vote(type) {
if (!this.currentMusicId) {
utils.toast('Aguarde a música carregar', 'error');
return;
}
try {
const mainBtn = type === 'like' ? $('#btn-like') : $('#btn-dislike');
const miniBtn = type === 'like' ? $('#mini-like') : $('#mini-dislike');
const otherMainBtn = type === 'like' ? $('#btn-dislike') : $('#btn-like');
const otherMiniBtn = type === 'like' ? $('#mini-dislike') : $('#mini-like');
const voteValue = type === 'like' ? 1 : -1;
// Pegar título e artista atuais para salvar junto com o voto
const title = els.songTitle ? els.songTitle.textContent : '';
const artist = els.songArtist ? els.songArtist.textContent : '';
const voteForm = new FormData();
voteForm.append('hash', CONFIG.hash);
voteForm.append('music_id', this.currentMusicId);
voteForm.append('type', type);
voteForm.append('music_title', title);
voteForm.append('music_artist', artist);
const res = await fetch(`${CONFIG.apiBase}/vote_music.php`, { method: 'POST', body: voteForm });
const data = await res.json();
if (data.success) {
// Atualizar contadores
if (els.likeCount) els.likeCount.textContent = data.likes || 0;
if (els.dislikeCount) els.dislikeCount.textContent = data.dislikes || 0;
// Atualizar botões baseado no voto retornado
[mainBtn, miniBtn, otherMainBtn, otherMiniBtn].forEach(btn => {
if (btn) btn.classList.remove('active');
});
if (data.user_vote === 1) {
if ($('#btn-like')) $('#btn-like').classList.add('active');
if ($('#mini-like')) $('#mini-like').classList.add('active');
this.triggerVoteEffect($('#btn-like'), 'like');
this.triggerVoteEffect($('#mini-like'), 'like');
utils.toast('Você curtiu!', 'success');
} else if (data.user_vote === -1) {
if ($('#btn-dislike')) $('#btn-dislike').classList.add('active');
if ($('#mini-dislike')) $('#mini-dislike').classList.add('active');
this.triggerVoteEffect($('#btn-dislike'), 'dislike');
this.triggerVoteEffect($('#mini-dislike'), 'dislike');
utils.toast('Voto registrado', 'success');
} else {
utils.toast('Voto removido', 'info');
}
} else {
utils.toast(data.message || 'Erro ao votar', 'error');
}
} catch (e) {
console.error('Erro ao votar:', e);
utils.toast('Erro ao votar', 'error');
}
}
};
// History
const history = {
async load() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/get_player_history.php?hash=${CONFIG.hash}`);
const data = await res.json();
if (data.success && data.data) {
// AzuraCast retorna song_history dentro de data
const historyItems = data.data.song_history || data.data;
if (Array.isArray(historyItems) && historyItems.length > 0) {
this.render(historyItems.slice(0, 8));
} else {
this.renderEmpty();
}
} else {
this.renderEmpty();
}
} catch (e) {
console.error('Erro ao carregar histórico:', e);
this.renderEmpty();
}
},
render(items) {
els.historyGrid.innerHTML = items.map(item => {
// Suporta tanto formato AzuraCast quanto formato simplificado
const song = item.song || item;
const title = song.title || song.text || 'Desconhecido';
const artist = song.artist || '';
const art = song.art || song.cover || 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const playedAt = item.played_at || item.sh_id || 0;
const searchQuery = encodeURIComponent(`${title} ${artist}`);
return `
${utils.escape(title)}
${utils.escape(artist)}
${artist ? ` ` : ''}
${playedAt ? utils.formatTime(playedAt * 1000) : ''}
`}).join('');
},
renderEmpty() {
if (!els.historyGrid) return;
els.historyGrid.innerHTML = `
Nenhuma música no histórico ainda
`;
}
};
// Locutores
const locutores = {
currentSlide: 0,
itemsPerSlide: 3,
totalItems: 0,
autoPlayInterval: null,
autoPlayDelay: 6000, // 6 segundos
async load() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/app_locutores.php?hash=${CONFIG.hash}`);
const data = await res.json();
// API returns { success: true, data: [...] }
const items = data.data || data.locutores || [];
if (data.success && items.length > 0) {
this.render(items);
} else {
this.renderEmpty();
}
} catch (e) {
this.renderEmpty();
}
},
render(items) {
const container = $('#dj-grid');
this.totalItems = items.length;
// Se tiver 3 ou menos, mostrar grid normal
if (items.length <= 3) {
container.className = 'dj-grid';
container.innerHTML = items.map(dj => this.renderCard(dj)).join('');
return;
}
// Se tiver mais de 3, criar slider (navega 1 em 1)
container.className = 'dj-grid dj-slider';
this.maxSlide = Math.max(0, items.length - 3);
container.innerHTML = `
${items.map(dj => this.renderCard(dj)).join('')}
${Array.from({length: items.length}, (_, i) =>
` `
).join('')}
`;
this.currentSlide = 0;
this.initSlider();
this.startAutoPlay();
},
renderCard(dj) {
return `
${utils.escape(dj.display_name || dj.nome || 'Locutor')}
${utils.escape(dj.comments || dj.funcao || 'Locutor')}
`;
},
initSlider() {
const prevBtn = $('#dj-prev');
const nextBtn = $('#dj-next');
const dots = document.querySelectorAll('#dj-dots .slider-dot');
const container = $('#dj-grid');
prevBtn?.addEventListener('click', () => {
this.goToSlide(this.currentSlide - 1);
this.resetAutoPlay();
});
nextBtn?.addEventListener('click', () => {
this.goToSlide(this.currentSlide + 1);
this.resetAutoPlay();
});
dots.forEach(dot => {
dot.addEventListener('click', () => {
this.goToSlide(parseInt(dot.dataset.slide));
this.resetAutoPlay();
});
});
// Pausar auto-play ao passar o mouse
container?.addEventListener('mouseenter', () => this.stopAutoPlay());
container?.addEventListener('mouseleave', () => this.startAutoPlay());
this.updateSlider();
},
goToSlide(index) {
// Loop infinito
if (index < 0) index = this.maxSlide;
if (index > this.maxSlide) index = 0;
this.currentSlide = index;
this.updateSlider();
},
updateSlider() {
const wrapper = $('#dj-slider-wrapper');
const dots = document.querySelectorAll('#dj-dots .slider-dot');
const prevBtn = $('#dj-prev');
const nextBtn = $('#dj-next');
if (!wrapper) return;
// Calcular offset baseado no tamanho do card + gap (1 em 1)
const card = wrapper.querySelector('.dj-card');
if (!card) return;
const cardWidth = card.offsetWidth;
const gap = 20;
const offset = this.currentSlide * (cardWidth + gap);
wrapper.style.transform = `translateX(-${offset}px)`;
// Atualizar dots - destacar os 3 visiveis
dots.forEach((dot, i) => {
const isVisible = i >= this.currentSlide && i < this.currentSlide + 3;
dot.classList.toggle('active', isVisible);
});
// Atualizar botões (desabilitados apenas visualmente, loop infinito funciona)
if (prevBtn) prevBtn.disabled = false;
if (nextBtn) nextBtn.disabled = false;
},
startAutoPlay() {
this.stopAutoPlay();
this.autoPlayInterval = setInterval(() => {
this.goToSlide(this.currentSlide + 1);
}, this.autoPlayDelay);
},
stopAutoPlay() {
if (this.autoPlayInterval) {
clearInterval(this.autoPlayInterval);
this.autoPlayInterval = null;
}
},
resetAutoPlay() {
this.stopAutoPlay();
this.startAutoPlay();
},
renderEmpty() {
const el = $('#dj-grid');
if (!el) return;
el.innerHTML = `
Nenhum locutor cadastrado
`;
}
};
// Top Musicas
const topMusicas = {
isManual: false,
async load() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/app_top_musicas.php?hash=${CONFIG.hash}&_=${Date.now()}`);
const data = await res.json();
const items = data.songs || data.musicas || [];
this.isManual = data.modo === 'manual' || data.modo === 'ia';
if (data.success && items.length > 0) {
this.render(items.slice(0, 10));
} else {
this.renderEmpty();
}
} catch (e) {
this.renderEmpty();
}
},
getYtThumb(url) {
if (!url) return '';
const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
return m ? 'https://img.youtube.com/vi/' + m[1] + '/hqdefault.jpg' : '';
},
render(items) {
const isManual = this.isManual;
$('#top-list').innerHTML = items.map((m, i) => {
const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
const coverArt = m.art || m.cover || this.getYtThumb(m.video_url) || 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const uv = parseInt(m.user_vote || 0, 10);
return `
${m.position || i + 1}
${utils.escape(m.title || m.titulo || '')}
${utils.escape(m.artist || m.artista || '')}
${(m.artist || m.artista) ? ` ` : ''}
${isManual ? `
${m.likes || 0}
${m.dislikes || 0}
${m.video_url ? ` ` : ''}
` : `
${m.plays || m.votos || 0}
`}
`}).join('');
},
triggerTopVoteEffect(btn, type) {
if (!btn) return;
const color = type === 'like' ? '#22c55e' : '#ef4444';
const colorAlt = type === 'like' ? '#4ade80' : '#f87171';
btn.classList.remove('vote-animating');
void btn.offsetWidth;
btn.classList.add('vote-animating');
// Ring burst
const ring = document.createElement('div');
ring.className = 'vote-ring';
ring.style.borderColor = color;
btn.appendChild(ring);
// Particles
const count = 10;
for (let i = 0; i < count; i++) {
const p = document.createElement('div');
p.className = 'vote-particle';
const angle = (i / count) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
const dist = 20 + Math.random() * 20;
p.style.setProperty('--vp-x', `${Math.cos(angle) * dist}px`);
p.style.setProperty('--vp-y', `${Math.sin(angle) * dist}px`);
p.style.left = '50%';
p.style.top = '50%';
const size = 3 + Math.random() * 4;
p.style.width = size + 'px';
p.style.height = size + 'px';
p.style.marginLeft = -(size / 2) + 'px';
p.style.marginTop = -(size / 2) + 'px';
p.style.background = Math.random() > 0.5 ? color : colorAlt;
p.style.animationDelay = (Math.random() * 0.08) + 's';
btn.appendChild(p);
}
setTimeout(() => {
btn.classList.remove('vote-animating');
btn.querySelectorAll('.vote-particle, .vote-ring').forEach(el => el.remove());
}, 700);
},
async vote(musicId, voteType) {
const clickedBtn = document.querySelector(`.top-vote-btn[data-music-id="${musicId}"][data-vote="${voteType}"]`);
if (clickedBtn) this.triggerTopVoteEffect(clickedBtn, voteType);
try {
const voteForm = new FormData();
voteForm.append('hash', CONFIG.hash);
voteForm.append('music_id', musicId);
voteForm.append('type', voteType);
const res = await fetch(`${CONFIG.apiBase}/vote_music.php`, { method: 'POST', body: voteForm });
const data = await res.json();
if (data.success) {
// Atualiza contadores + estado ativo imediatamente,
// sem esperar reload (evita cache + timing).
const allBtns = document.querySelectorAll(`.top-vote-btn[data-music-id="${musicId}"]`);
allBtns.forEach(b => {
const isLike = b.dataset.vote === 'like';
const cnt = b.querySelector('.top-vote-btn__count');
if (cnt) cnt.textContent = String(isLike ? (data.likes || 0) : (data.dislikes || 0));
const shouldActive = (isLike && data.user_vote === 1) || (!isLike && data.user_vote === -1);
b.classList.toggle('voted', shouldActive);
});
utils.toast(voteType === 'like' ? 'Você curtiu!' : 'Voto registrado', 'success');
// Recarrega no fundo p/ reordenar ranking
setTimeout(() => this.load(), 600);
} else {
utils.toast(data.message || 'Erro ao votar', 'error');
}
} catch(e) {
utils.toast('Erro ao votar', 'error');
}
},
renderEmpty() {
const el = $('#top-list');
if (!el) return;
el.innerHTML = `
`;
}
};
// Top Vídeos
// Thumb Lightbox (abrir thumbnail em tela cheia)
function openThumbLightbox(src, title) {
let lb = document.querySelector('.thumb-lightbox');
if (lb) lb.remove();
lb = document.createElement('div');
lb.className = 'thumb-lightbox';
lb.innerHTML = `
×
${title ? `${utils.escape(title)}
` : ''}
`;
document.body.appendChild(lb);
requestAnimationFrame(() => lb.classList.add('active'));
const close = () => { lb.classList.remove('active'); setTimeout(() => lb.remove(), 300); };
lb.querySelector('.thumb-lightbox-close').addEventListener('click', close);
lb.addEventListener('click', (e) => { if (e.target === lb) close(); });
const onKey = (e) => { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); } };
document.addEventListener('keydown', onKey);
}
const topVideos = {
async load() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/app_top_videos.php?hash=${CONFIG.hash}`);
const data = await res.json();
if (data.success && data.videos && data.videos.length > 0) {
this.render(data.videos);
} else {
this.renderEmpty();
}
} catch (e) {
this.renderEmpty();
}
},
render(items) {
// Extrai artista do titulo "Artista - Musica" ou usa v.artist se vier
const extractArtist = (v) => {
if (v.artist) return String(v.artist).trim();
const t = String(v.title || '').trim();
if (!t) return null;
const m = t.match(/^(.+?)\s*[-–—]\s+/);
if (!m) return null;
const a = m[1].trim();
if (a.length < 2 || a.length > 80) return null;
return a;
};
$('#top-videos-list').innerHTML = `${items.map((v, i) => {
const rankClass = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
const thumb = v.thumbnail || 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const artist = extractArtist(v);
// Modo auto envia title e artist separados: junta "Artist - Title" pra exibir.
// Modo legado envia title ja como "Artist - Title": usa direto.
const displayTitle = v.artist ? `${String(v.artist).trim()} - ${String(v.title || '').trim()}` : (v.title || '');
return `
${v.position || i + 1}
${utils.escape(displayTitle)}
${artist ? ` ` : ''}
${v.description ? `
${utils.escape(v.description.substring(0, 80))}
` : '
'}
`}).join('')}
`;
},
async vote(videoId, voteType) {
const clickedBtn = document.querySelector(`.top-video-vote-btn[data-video-id="${videoId}"][data-vote="${voteType}"]`);
if (clickedBtn) topMusicas.triggerTopVoteEffect(clickedBtn, voteType);
try {
const voteForm = new FormData();
voteForm.append('hash', CONFIG.hash);
voteForm.append('video_id', videoId);
voteForm.append('type', voteType);
const res = await fetch(`${CONFIG.apiBase}/vote_video.php`, { method: 'POST', body: voteForm });
const data = await res.json();
if (data.success) {
utils.toast(voteType === 'like' ? 'Você curtiu!' : 'Voto registrado', 'success');
await this.load();
} else {
utils.toast(data.message || 'Erro ao votar', 'error');
}
} catch(e) {
utils.toast('Erro ao votar', 'error');
}
},
renderEmpty() {
$('#top-videos-list').innerHTML = `
`;
}
};
// Video Modal (Top Musicas + Top Videos) com PiP
let videoModalScrollY = 0;
let videoModalIsPip = false;
function openVideoModal(url) {
const modal = document.getElementById('video-modal-overlay');
const content = document.getElementById('video-modal-content');
if (!modal || !content) return;
const ytMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
if (ytMatch) {
content.innerHTML = `VIDEO `;
} else {
content.innerHTML = ` `;
}
modal.classList.remove('pip');
modal.classList.add('active');
videoModalIsPip = false;
videoModalScrollY = window.scrollY;
updatePipToggleIcon();
}
function closeVideoModal() {
const modal = document.getElementById('video-modal-overlay');
const content = document.getElementById('video-modal-content');
if (modal) modal.classList.remove('active', 'pip');
if (content) content.innerHTML = '';
videoModalIsPip = false;
}
function toggleVideoPip() {
const modal = document.getElementById('video-modal-overlay');
if (!modal || !modal.classList.contains('active')) return;
videoModalIsPip = !videoModalIsPip;
modal.classList.toggle('pip', videoModalIsPip);
updatePipToggleIcon();
}
function updatePipToggleIcon() {
const btn = document.getElementById('video-pip-toggle');
if (!btn) return;
const icon = btn.querySelector('i');
if (videoModalIsPip) {
icon.className = 'fas fa-expand-alt';
btn.title = 'Expandir';
} else {
icon.className = 'fas fa-compress-alt';
btn.title = 'Minimizar';
}
}
// Album de Fotos
const albumFotos = {
albums: [],
currentIdx: 0,
async load() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/app_album_fotos.php?hash=${CONFIG.hash}`);
const data = await res.json();
if (data.success && data.albums && data.albums.length > 0) {
this.albums = data.albums;
this.render();
} else {
this.renderEmpty();
}
} catch (e) {
this.renderEmpty();
}
},
render() {
this.renderAlbum(0);
},
photosPerPage: 12,
visibleCount: 12,
renderAlbum(idx, animate = true) {
const isSwitch = this.currentIdx !== idx && animate && !!$('#album-fotos-container .album-fotos-grid');
this.currentIdx = idx;
this.visibleCount = this.photosPerPage;
const container = $('#album-fotos-container');
if (!container) return;
const album = this.albums[idx];
const buildContent = () => {
let tabsHtml = '';
if (this.albums.length > 1) {
tabsHtml = '' +
this.albums.map((a, i) =>
`${utils.escape(a.nome)} `
).join('') + '
';
}
const visible = album.fotos.slice(0, this.visibleCount);
const remaining = album.fotos.length - this.visibleCount;
const albumNome = utils.escape(album.nome);
const gridHtml = '' +
visible.map((f, fi) =>
`
' +
`
` +
`
${albumNome}
` +
'
'
).join('') + '
';
const moreBtn = remaining > 0
? ` Ver mais ${remaining} foto${remaining > 1 ? 's' : ''}
`
: '';
container.innerHTML = tabsHtml + gridHtml + moreBtn;
};
if (isSwitch) {
// Atualizar tab ativa imediatamente
container.querySelectorAll('.album-fotos-tab').forEach(t => {
t.classList.toggle('active', parseInt(t.dataset.albumIdx) === idx);
});
// Fade out do grid atual
const grid = container.querySelector('.album-fotos-grid');
if (grid) {
grid.classList.add('grid-fade-out');
setTimeout(() => buildContent(), 300);
} else {
buildContent();
}
} else {
buildContent();
}
},
showMore() {
this.visibleCount += this.photosPerPage;
const container = $('#album-fotos-container');
if (!container) return;
const album = this.albums[this.currentIdx];
const visible = album.fotos.slice(0, this.visibleCount);
const remaining = album.fotos.length - this.visibleCount;
const grid = container.querySelector('.album-fotos-grid');
if (grid) {
const albumNome = utils.escape(album.nome);
grid.innerHTML = visible.map((f, fi) =>
`` +
`
` +
`
${albumNome}
` +
'
'
).join('');
}
const btnWrap = container.querySelector('.album-fotos-ver-mais')?.parentElement;
if (remaining > 0 && btnWrap) {
btnWrap.innerHTML = ` Ver mais ${remaining} foto${remaining > 1 ? 's' : ''} `;
} else if (btnWrap) {
btnWrap.remove();
}
},
openLightbox(photos, startIdx) {
let idx = startIdx;
const album = this.albums[this.currentIdx];
const albumNome = album ? album.nome : '';
const lb = document.createElement('div');
lb.className = 'photo-lightbox';
lb.innerHTML = `
×
${photos.length > 1 ? ' ' : ''}
${photos.length > 1 ? ' ' : ''}
${albumNome} ${photos.length > 1 ? `(${idx + 1}/${photos.length})` : ''}
`;
document.body.appendChild(lb);
requestAnimationFrame(() => lb.classList.add('active'));
const img = lb.querySelector('.photo-lightbox-img');
const caption = lb.querySelector('.photo-lightbox-caption');
function show(i) {
idx = i;
img.src = photos[idx].foto;
caption.textContent = `${albumNome} (${idx + 1}/${photos.length})`;
}
function nav(dir) {
let next = idx + dir;
if (next < 0) next = photos.length - 1;
if (next >= photos.length) next = 0;
show(next);
}
function close() {
lb.classList.remove('active');
setTimeout(() => lb.remove(), 300);
document.removeEventListener('keydown', onKey);
}
function onKey(e) {
if (e.key === 'Escape') close();
if (e.key === 'ArrowLeft') nav(-1);
if (e.key === 'ArrowRight') nav(1);
}
document.addEventListener('keydown', onKey);
lb.querySelector('.photo-lightbox-close').addEventListener('click', close);
lb.addEventListener('click', (e) => { if (e.target === lb) close(); });
const prevBtn = lb.querySelector('.photo-lightbox-nav.prev');
const nextBtn = lb.querySelector('.photo-lightbox-nav.next');
if (prevBtn) prevBtn.addEventListener('click', () => nav(-1));
if (nextBtn) nextBtn.addEventListener('click', () => nav(1));
},
renderEmpty() {
const container = $('#album-fotos-container');
if (container) {
container.innerHTML = `
`;
}
}
};
// Pedidos de Musica
const pedidos = {
searchInput: $('#pedido-search'),
list: $('#pedidos-list'),
agentFields: $('#pedidos-agent-fields'),
allSongs: [], // só página atual, mantido para compat com requestSong()
isAgentMode: false,
blocked: false,
requestedIds: new Set(),
currentPage: 1,
totalPages: 1,
total: 0,
rowCount: 30,
search: '',
loading: false,
searchTimer: null,
init() {
if (!this.list) return;
this.loadPage(1);
if (this.searchInput) {
this.searchInput.addEventListener('input', () => this.onSearchInput());
}
},
onSearchInput() {
clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => {
const q = (this.searchInput?.value || '').trim();
this.search = q;
this.loadPage(1);
}, 400);
},
async loadPage(page) {
if (this.blocked) return;
if (this.loading) return;
this.loading = true;
this.renderListLoading();
try {
const params = new URLSearchParams({
action: 'list',
hash: CONFIG.hash,
rowCount: String(this.rowCount),
current: String(page),
});
if (this.search) params.set('searchPhrase', this.search);
const res = await fetchWithRetry(`${CONFIG.apiBase}/get_player_requests.php?${params.toString()}`);
const data = await res.json();
if (data.blocked) {
this.applyBlockedState(data);
return;
}
let songs = [];
let total = 0, totalPages = 1, current = 1;
if (data.mode === 'agent' && Array.isArray(data.rows)) {
this.isAgentMode = true;
songs = data.rows.map(r => ({
_agent: true,
id: r.song_id,
title: r.title || 'Desconhecido',
artist: r.artist || '',
art: r.art || '',
song_id: r.song_id,
song_path: r.song_path,
playlist_id: r.playlist_id,
}));
total = data.total || songs.length;
totalPages = data.totalPages || 1;
current = data.current || page;
} else if (data.success && data.data && Array.isArray(data.data.rows)) {
this.isAgentMode = false;
songs = data.data.rows.map(r => ({
_agent: false,
id: r.request_id,
title: r.song?.title || 'Desconhecido',
artist: r.song?.artist || '',
art: r.song?.art || '',
request_id: r.request_id,
}));
total = data.data.total || songs.length;
totalPages = data.data.total_pages || 1;
current = data.data.page || page;
}
this.currentPage = current;
this.totalPages = totalPages;
this.total = total;
if (this.agentFields) {
this.agentFields.style.display = this.isAgentMode ? 'flex' : 'none';
}
this.allSongs = songs;
if (songs.length > 0) {
this.render(songs);
} else if (this.search) {
this.renderEmpty('Nenhuma música encontrada');
} else {
this.renderEmpty(data.message || 'Nenhuma música disponível para pedido');
}
} catch (e) {
console.error('Erro ao carregar pedidos:', e);
this.renderEmpty('Erro ao carregar músicas');
} finally {
this.loading = false;
}
},
renderListLoading() {
this.list.innerHTML = `
Carregando músicas...
`;
},
render(songs) {
const defaultArt = 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const itemsHtml = songs.map(song => {
const already = this.requestedIds.has(String(song.id));
const disabled = already ? 'disabled' : '';
const btnHtml = already
? ' Pedido!'
: ' Pedir';
return `
${utils.escape(song.title || 'Desconhecido')}
${utils.escape(song.artist || '')}
${btnHtml}
`;
}).join('');
this.list.innerHTML = itemsHtml + this.renderPagination();
this.list.querySelectorAll('.pedido-item__btn').forEach(btn => {
btn.onclick = () => this.requestSong(btn);
});
this.list.querySelectorAll('.pedidos-pagination__btn[data-page]').forEach(btn => {
btn.onclick = () => {
const p = parseInt(btn.dataset.page, 10);
if (!isNaN(p) && p !== this.currentPage) this.loadPage(p);
};
});
},
renderPagination() {
if (this.totalPages <= 1) return '';
const cur = this.currentPage;
const tp = this.totalPages;
const pages = this.buildPageList(cur, tp);
const btn = (label, page, opts = {}) => {
const cls = ['pedidos-pagination__btn'];
if (opts.active) cls.push('active');
const dis = opts.disabled ? 'disabled' : '';
const dataPage = page != null ? `data-page="${page}"` : '';
return `${label} `;
};
const parts = [];
parts.push(btn('‹', cur - 1, { disabled: cur <= 1 }));
for (const p of pages) {
if (p === '…') {
parts.push(``);
} else {
parts.push(btn(String(p), p, { active: p === cur, disabled: p === cur }));
}
}
parts.push(btn('›', cur + 1, { disabled: cur >= tp }));
const info = `Página ${cur} de ${tp} • ${this.total} música${this.total === 1 ? '' : 's'}`;
return `
`;
},
buildPageList(current, total) {
// Produz [1, '…', 4, 5, 6, '…', 12] com janela de ±2 ao redor de current
const window = 2;
const set = new Set();
set.add(1);
set.add(total);
for (let p = current - window; p <= current + window; p++) {
if (p >= 1 && p <= total) set.add(p);
}
const sorted = Array.from(set).sort((a, b) => a - b);
const out = [];
let prev = 0;
for (const p of sorted) {
if (prev && p - prev > 1) out.push('…');
out.push(p);
prev = p;
}
return out;
},
renderEmpty(msg) {
this.list.innerHTML = `
`;
},
// Validação local (mesmo regex do embed/site2)
validateNome(nome) {
if (!nome) return 'Informe seu nome para solicitar.';
if (!/^[\p{L}\s'\-]{2,50}$/u.test(nome)) return 'Nome inválido. Use apenas letras (2 a 50 caracteres).';
if (((nome.match(/\p{L}/gu) || []).length) < 2) return 'Informe um nome válido.';
return null;
},
validateCidade(cidade) {
if (!cidade) return null;
if (!/^[\p{L}\s,\-.]{2,50}$/u.test(cidade)) return 'Cidade inválida. Use apenas letras (ex: "São Paulo" ou "Rio, RJ").';
return null;
},
async requestSong(btn) {
if (this.blocked) return;
const rowId = btn.dataset.rowId;
const song = this.allSongs.find(s => String(s.id) === String(rowId));
if (!song) { utils.toast('Música inválida', 'error'); return; }
if (this.requestedIds.has(String(rowId))) {
utils.toast('Esta música já foi pedida.', 'warning');
return;
}
// Montar payload
let payload;
if (song._agent) {
const nomeEl = $('#pedido-nome');
const cidadeEl = $('#pedido-cidade');
const nome = (nomeEl?.value || '').trim();
const cidade = (cidadeEl?.value || '').trim();
const errName = this.validateNome(nome);
if (errName) { utils.toast(errName, 'error'); nomeEl?.focus(); return; }
const errCity = this.validateCidade(cidade);
if (errCity) { utils.toast(errCity, 'error'); cidadeEl?.focus(); return; }
payload = {
action: 'request',
hash: CONFIG.hash,
song_id: song.song_id,
song_path: song.song_path,
song_title: song.title,
playlist_id: song.playlist_id,
nome,
cidade,
};
} else {
payload = { action: 'request', hash: CONFIG.hash, request_id: song.request_id };
}
const originalContent = btn.innerHTML;
btn.innerHTML = ' ';
btn.disabled = true;
try {
const res = await fetch(`${CONFIG.apiBase}/get_player_requests.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
console.log('[Pedidos] Response:', data);
// Bloqueio acionado
if (data.blocked) {
this.applyBlockedState(data);
return;
}
if (data.warning) utils.toast(data.warning, 'error');
if (data.success) {
this.requestedIds.add(String(rowId));
utils.toast(data.message || 'Música pedida com sucesso! Aguarde que será tocada em breve.', 'success');
btn.innerHTML = ' Pedido!';
// Liberar após 5 minutos pra permitir novo pedido da mesma música
setTimeout(() => {
this.requestedIds.delete(String(rowId));
btn.innerHTML = originalContent;
btn.disabled = false;
}, 300000);
} else {
if (data.message?.includes('duplicado') || data.message?.includes('duplicate') || data.message?.includes('ja está')) {
utils.toast('Esta música já está na fila.', 'warning');
} else {
utils.toast(data.message || 'Erro ao pedir música', 'error');
}
btn.innerHTML = originalContent;
btn.disabled = false;
}
} catch (e) {
console.error('Erro ao pedir música:', e);
utils.toast('Erro ao pedir música. Tente novamente.', 'error');
btn.innerHTML = originalContent;
btn.disabled = false;
}
},
// Painel de bloqueio com countdown
applyBlockedState(info) {
this.blocked = true;
const reason = info.reason || 'Conteúdo impróprio detectado';
const remainingSec = parseInt(info.remaining || 0, 10);
const message = info.message || 'Seu acesso está bloqueado.';
// Desabilitar UI
if (this.searchInput) this.searchInput.disabled = true;
if (this.agentFields) this.agentFields.style.display = 'none';
// Mostrar painel
this.list.innerHTML = `
⚠️
Acesso Bloqueado
${utils.escape(message)}
Motivo: ${utils.escape(reason)}
${remainingSec > 0 ? `
` : ''}
`;
if (remainingSec > 0) {
const el = $('#pedidoBlockCountdown');
let rem = remainingSec;
const tick = () => {
if (rem <= 0) { el.textContent = 'Bloqueio expirado. Recarregue a página.'; return; }
const h = Math.floor(rem / 3600);
const m = Math.floor((rem % 3600) / 60);
const s = rem % 60;
const parts = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
parts.push(`${s}s`);
el.textContent = `Tempo restante: ${parts.join(' ')}`;
rem--;
};
tick();
setInterval(tick, 1000);
}
utils.toast(message, 'error');
}
};
// Noticias
const noticias = {
visibleCount: 0,
perPage: CONFIG.noticiasLimite || 6,
activeTab: 'todos',
allItems: [],
async load() {
if (!els.newsGrid) return;
// Multi-país: buscar fontes via endpoint para obter config atualizada
const fontes = CONFIG.noticiasFontes || {};
const hasBrasil = !!fontes.brasil;
const hasPortugal = !!fontes.portugal;
if (hasBrasil || hasPortugal) {
await this.loadMultiPais(fontes);
return;
}
// Legacy: fonte única
if (!CONFIG.rssUrl) return;
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/rss_proxy.php?url=${encodeURIComponent(CONFIG.rssUrl)}`);
const data = await res.json();
if (data.items && data.items.length > 0) {
this.render(data.items.slice(0, CONFIG.noticiasLimite || 6));
} else {
this.renderEmpty(data.error || 'Nenhuma notícia encontrada');
}
} catch (e) {
this.renderEmpty('Erro ao carregar notícias');
}
},
async loadMultiPais(fontes) {
const limit = CONFIG.noticiasLimite || 6;
const fetches = [];
const paises = [];
if (fontes.brasil) {
fetches.push(fetchWithRetry(`${CONFIG.apiBase}/rss_proxy.php?url=${encodeURIComponent(fontes.brasil)}`).then(r => r.json()).catch(() => ({ items: [] })));
paises.push('brasil');
}
if (fontes.portugal) {
fetches.push(fetchWithRetry(`${CONFIG.apiBase}/rss_proxy.php?url=${encodeURIComponent(fontes.portugal)}`).then(r => r.json()).catch(() => ({ items: [] })));
paises.push('portugal');
}
const results = await Promise.all(fetches);
let allItems = [];
results.forEach((data, i) => {
const items = (data.items || []).slice(0, limit);
items.forEach(item => { item._pais = paises[i]; });
allItems = allItems.concat(items);
});
// Ordenar por data (mais recente primeiro)
allItems.sort((a, b) => {
const da = new Date(a.pubDate || 0);
const db = new Date(b.pubDate || 0);
return db - da;
});
this.allItems = allItems;
// Mostrar tabs se ambos países têm dados
const paisesComDados = [...new Set(allItems.map(i => i._pais))];
const tabsEl = document.getElementById('news-tabs');
if (tabsEl && paisesComDados.length > 1) {
tabsEl.style.display = 'flex';
this.initTabs();
}
if (allItems.length > 0) {
this.renderFiltered();
} else {
this.renderEmpty('Nenhuma notícia encontrada');
}
},
initTabs() {
const tabsEl = document.getElementById('news-tabs');
if (!tabsEl || tabsEl._tabsInit) return;
tabsEl._tabsInit = true;
tabsEl.addEventListener('click', (e) => {
const btn = e.target.closest('.news-tab');
if (!btn) return;
tabsEl.querySelectorAll('.news-tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
this.activeTab = btn.dataset.pais;
this.renderFiltered();
});
},
renderFiltered() {
const filtered = this.activeTab === 'todos'
? this.allItems
: this.allItems.filter(i => i._pais === this.activeTab);
this.items = filtered;
this.render(filtered);
},
formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (hours < 1) return 'Agora';
if (hours < 24) return `${hours}h atras`;
if (days < 7) return `${days}d atras`;
return date.toLocaleDateString('pt-BR');
} catch (e) {
return '';
}
},
renderCard(item, index, featured = false) {
const defaultImg = 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const hasContent = item.content && item.content.trim();
const hasLink = item.link && item.link.trim();
const authorDate = [];
if (item.author) authorDate.push(` ${utils.escape(item.author)}`);
if (item.pubDate) authorDate.push(` ${this.formatDate(item.pubDate)}`);
const metaHtml = authorDate.length ? authorDate.join(' ') : ` ${this.formatDate(item.pubDate)}`;
// Badge de país
const paisBadge = item._pais
? `${item._pais === 'brasil' ? '\u{1F1E7}\u{1F1F7}' : '\u{1F1F5}\u{1F1F9}'} `
: '';
const featuredClass = featured ? ' news-card--featured' : '';
const badgeHtml = featured ? ' Destaque ' : '';
const descLimit = featured ? 250 : 150;
if (hasContent) {
return `
${badgeHtml}
${paisBadge}${metaHtml}
${utils.escape(item.title)}
${utils.escape(item.description?.substring(0, descLimit) || '')}
`;
}
return `
${badgeHtml}
${paisBadge}${metaHtml}
${utils.escape(item.title)}
${utils.escape(item.description?.substring(0, descLimit) || '')}
`;
},
render(items) {
this.items = items;
this.visibleCount = Math.min(this.perPage, items.length);
let html = '';
items.slice(0, this.visibleCount).forEach((item, index) => {
html += this.renderCard(item, index, index === 0);
// Insert in-article ad after 3rd item
if (index === 2 && CONFIG.adsense.slotInarticle) {
html += utils.createAdUnit(CONFIG.adsense.slotInarticle, 'fluid', 'display:block;text-align:center;');
}
});
// Botão "Ver mais" se houver mais itens
if (items.length > this.visibleCount) {
html += `
Ver mais noticias
(+${items.length - this.visibleCount})
`;
}
els.newsGrid.innerHTML = html;
// Initialize dynamic ads
setTimeout(() => utils.initDynamicAds(), 100);
},
renderEmpty(msg) {
if (!els.newsGrid) return;
els.newsGrid.innerHTML = `
`;
}
};
// Blog da Rádio (paginacao por servidor: append na proxima leva)
const blogRadio = {
items: [],
perPage: CONFIG.blogLimite || 6,
visibleCount: 0,
offset: 0,
total: 0,
hasMore: false,
async load() {
const section = document.getElementById('blog-radio');
const grid = document.getElementById('blog-grid');
if (!section || !grid) return;
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/get_blog_posts.php?hash=${CONFIG.hash}&limit=${this.perPage}&offset=0`);
const data = await res.json();
if (!data.success || !data.items?.length) {
section.style.display = 'none';
return;
}
// Dados da primeira pagina
this.items = data.items;
this.offset = data.items.length;
this.total = data.total ?? data.items.length;
this.hasMore = !!data.has_more;
this.render(data.items);
this.loaded = true;
window.blogRadioLoaded = true;
// Se siteConfig já carregou, respeitar ativo
// Se não carregou, applyModuleVisibility vai controlar depois
if (window.siteConfig?.modulos?.blog_radio) {
const cfg = window.siteConfig.modulos.blog_radio;
const isDesativado = cfg.ativo === false || cfg.ativo === 0 || cfg.ativo === '0' || cfg.ativo === 'false';
section.style.display = isDesativado ? 'none' : '';
if (!isDesativado) {
const navBlog = document.getElementById('nav-blog-radio');
const footerBlog = document.getElementById('footer-blog-radio');
if (navBlog) navBlog.style.display = '';
if (footerBlog) footerBlog.style.display = '';
if (window.adjustNavOverflow) window.adjustNavOverflow();
}
}
// Se não tem config ainda, mantém display:none (padrão)
// applyModuleVisibility vai setar display:'' se ativo
} catch (e) {
section.style.display = 'none';
}
},
formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (hours < 1) return 'Agora';
if (hours < 24) return `${hours}h atrás`;
if (days < 7) return `${days}d atrás`;
return date.toLocaleDateString('pt-BR');
} catch (e) {
return '';
}
},
renderCard(item, index, featured) {
const defaultImg = 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const img = item.thumbnail || defaultImg;
const hasContent = item.content && item.content.trim();
const hasLink = item.link && item.link.trim();
const metaParts = [];
if (item.author) metaParts.push(` ${utils.escape(item.author)}`);
if (item.pubDate) metaParts.push(` ${this.formatDate(item.pubDate)}`);
const metaHtml = metaParts.join(' ');
const clickAction = hasContent
? `onclick="openBlogModal(${index})" style="cursor:pointer;"`
: (hasLink ? `onclick="window.open('${utils.escape(item.link)}','_blank')" style="cursor:pointer;"` : '');
if (featured) {
return `
Da Rádio
${utils.escape(item.title)}
${utils.escape(item.description?.substring(0, 200) || '')}
${metaHtml}
${hasContent ? '' : ''}
`;
}
return `
Da Rádio
${utils.escape(item.title)}
${utils.escape(item.description?.substring(0, 120) || '')}
${metaHtml}
${hasContent ? '' : ''}
`;
},
render(items) {
const grid = document.getElementById('blog-grid');
if (!grid) return;
this.visibleCount = items.length;
let html = '';
items.forEach((item, i) => {
if (i === 0) {
html += this.renderCard(item, i, true);
html += '';
} else {
html += this.renderCard(item, i, false);
}
});
if (this.visibleCount > 1) html += '
';
if (this.hasMore) {
const restante = Math.max(0, this.total - this.items.length);
html += `
Ver mais posts
(+${restante})
`;
}
grid.innerHTML = html;
}
};
// Programação
const programacao = {
currentSlide: 0,
itemsPerSlide: 3,
totalItems: 0,
autoPlayInterval: null,
autoPlayDelay: 5000, // 5 segundos
programasData: [],
async load() {
const modo = CONFIG.modoProgramacao || 'automatica';
// Modo Simples: exibe texto livre
if (modo === 'simples') {
this.renderSimples(CONFIG.programacaoTexto);
return;
}
// Modo Elaborada: busca programas cadastrados
if (modo === 'elaborada') {
await this.loadElaborada();
return;
}
// Modo Automatica (padrão): busca do AzuraCast/Locutores
if (!CONFIG.azuracastUrl || !CONFIG.stationId) {
this.renderEmpty();
return;
}
try {
// Buscar direto do AzuraCast (igual app6.php)
const res = await fetchWithRetry(`${CONFIG.azuracastUrl}/api/station/${CONFIG.stationId}/schedule?t=${Date.now()}`);
const data = await res.json();
if (Array.isArray(data) && data.length > 0) {
this.render(data);
} else {
// Tentar buscar streamers como fallback
await this.loadFromStreamers();
}
} catch (e) {
console.error('Erro ao carregar programação:', e);
// Tentar buscar streamers como fallback
await this.loadFromStreamers();
}
},
// Modo Simples: exibe texto livre formatado
renderSimples(texto) {
const grid = $('#schedule-grid');
if (!grid) return;
if (!texto || texto.trim() === '') {
this.renderEmpty();
return;
}
grid.className = 'schedule-grid';
grid.innerHTML = `
${utils.escape(texto.replace(/\\n/g, '\n'))}
`;
},
// Modo Elaborada: busca programas e exibe com abas
async loadElaborada() {
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/get_programacao_site.php?hash=${CONFIG.hash}`);
const data = await res.json();
if (data.success && data.data && data.data.length > 0) {
this.programasData = data.data;
this.renderElaborada();
} else {
this.renderEmpty();
}
} catch (e) {
console.error('Erro ao carregar programação elaborada:', e);
this.renderEmpty();
}
},
// Slider state para modo elaborada
currentSlideElab: 0,
maxSlideElab: 0,
autoPlayIntervalElab: null,
autoPlayDelayElab: 5000,
renderElaborada() {
const grid = $('#schedule-grid');
if (!grid) return;
// Usar fuso horário da rádio para determinar dia atual
const tzTime = this.getTimeInTimezone();
const hoje = tzTime.dayOfWeek;
const diasNomes = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
grid.className = 'schedule-grid schedule-elaborada';
grid.innerHTML = `
${diasNomes.map((dia, i) => `
${dia}
`).join('')}
`;
// Event listeners nas abas
grid.querySelectorAll('.tab-btn-dia').forEach(btn => {
btn.addEventListener('click', (e) => {
const dia = parseInt(btn.dataset.dia);
this.stopAutoPlayElab();
this.showDia(dia);
// Atualizar estilos das abas
grid.querySelectorAll('.tab-btn-dia').forEach(b => {
const isActive = parseInt(b.dataset.dia) === dia;
b.style.background = isActive ? 'var(--dynamic-primary)' : 'rgba(255,255,255,0.1)';
b.style.color = isActive ? 'white' : 'rgba(255,255,255,0.7)';
b.style.fontWeight = isActive ? '600' : '400';
b.classList.toggle('active', isActive);
});
});
});
// Mostrar dia atual
this.showDia(hoje);
},
// Obter hora atual no fuso horário da rádio
getTimeInTimezone() {
const tz = CONFIG.timezone || 'America/Sao_Paulo';
try {
const now = new Date();
// Criar data no fuso horário especificado
const formatter = new Intl.DateTimeFormat('pt-BR', {
timeZone: tz,
hour: '2-digit',
minute: '2-digit',
weekday: 'short',
hour12: false
});
const parts = formatter.formatToParts(now);
let h = 0, m = 0, dayStr = '';
parts.forEach(part => {
if (part.type === 'hour') h = parseInt(part.value);
if (part.type === 'minute') m = parseInt(part.value);
if (part.type === 'weekday') dayStr = part.value.toLowerCase().replace('.', '');
});
const dayMap = { 'dom': 0, 'seg': 1, 'ter': 2, 'qua': 3, 'qui': 4, 'sex': 5, 'sáb': 6, 'sab': 6 };
const dayOfWeek = dayMap[dayStr] ?? now.getDay();
return { hours: h, minutes: m, dayOfWeek, totalMinutes: h * 60 + m };
} catch (e) {
console.error('[Programação] Erro ao obter fuso horário:', e);
const now = new Date();
return {
hours: now.getHours(),
minutes: now.getMinutes(),
dayOfWeek: now.getDay(),
totalMinutes: now.getHours() * 60 + now.getMinutes()
};
}
},
showDia(dia) {
// Filtrar programas do dia
const programasDia = this.programasData.filter(p =>
p.dias_semana.split(',').includes(String(dia))
).sort((a, b) => a.hora_inicio.localeCompare(b.hora_inicio));
const content = document.getElementById('programacao-content');
if (!content) return;
// Usar fuso horário da rádio
const tzTime = this.getTimeInTimezone();
const horaAtual = tzTime.totalMinutes;
const ehHoje = tzTime.dayOfWeek === dia;
if (programasDia.length === 0) {
content.innerHTML = `
Sem programação para este dia
`;
return;
}
// Logo para usar quando não tem imagem
const logoUrl = CONFIG.logoUrl || '';
// Encontrar programa atual
let nowIndex = 0;
programasDia.forEach((p, index) => {
const [h, m] = (p.hora_inicio || '00:00').split(':').map(Number);
const [hf, mf] = (p.hora_fim || '23:59').split(':').map(Number);
const inicioMin = h * 60 + m;
let fimMin = hf * 60 + mf;
// Se hora fim for 00:00 ou menor que início, tratar como 24:00
if (fimMin === 0 || fimMin <= inicioMin) {
fimMin = 1440;
}
if (ehHoje && horaAtual >= inicioMin && horaAtual < fimMin) {
nowIndex = index;
}
});
// Se tiver 3 ou menos, grid normal
if (programasDia.length <= 3) {
content.innerHTML = `${programasDia.map(p => this.renderItemElab(p, ehHoje, horaAtual, logoUrl)).join('')}
`;
return;
}
// Se tiver mais de 3, criar slider
const maxSlide = Math.max(0, programasDia.length - 3);
const startSlide = Math.min(nowIndex, maxSlide);
content.innerHTML = `
${programasDia.map(p => this.renderItemElab(p, ehHoje, horaAtual, logoUrl)).join('')}
${Array.from({length: programasDia.length}, (_, i) =>
` `
).join('')}
`;
this.currentSlideElab = startSlide;
this.maxSlideElab = maxSlide;
this.initSliderElab();
this.startAutoPlayElab();
},
renderItemElab(p, ehHoje, horaAtual, logoUrl) {
const horaInicio = p.hora_inicio ? p.hora_inicio.slice(0,5) : '00:00';
const horaFim = p.hora_fim ? p.hora_fim.slice(0,5) : '23:59';
const [h, m] = horaInicio.split(':').map(Number);
const [hf, mf] = horaFim.split(':').map(Number);
const inicioMin = h * 60 + m;
// Se hora fim for 00:00, tratar como 24:00 (meia-noite)
let fimMin = hf * 60 + mf;
if (fimMin === 0 || fimMin <= inicioMin) {
fimMin = 1440; // 24:00 = 1440 minutos
}
const isNow = ehHoje && horaAtual >= inicioMin && horaAtual < fimMin;
// Usar imagem do programa ou logo da rádio
let imagemUrl = p.imagem ? (p.imagem.startsWith('http') ? p.imagem : CONFIG.baseUrl + '/' + p.imagem) : logoUrl;
return `
${utils.escape(p.nome_programa || 'Programa')}
${p.locutor ? utils.escape(p.locutor) : (p.descricao ? utils.escape(p.descricao) : '')}
`;
},
initSliderElab() {
const prevBtn = $('#elab-prev');
const nextBtn = $('#elab-next');
const dots = document.querySelectorAll('#elab-dots .slider-dot');
prevBtn?.addEventListener('click', () => {
this.goToSlideElab(this.currentSlideElab - 1);
this.resetAutoPlayElab();
});
nextBtn?.addEventListener('click', () => {
this.goToSlideElab(this.currentSlideElab + 1);
this.resetAutoPlayElab();
});
dots.forEach(dot => {
dot.addEventListener('click', () => {
this.goToSlideElab(parseInt(dot.dataset.slide));
this.resetAutoPlayElab();
});
});
this.updateSliderElab();
},
goToSlideElab(index) {
if (index < 0) index = this.maxSlideElab;
if (index > this.maxSlideElab) index = 0;
this.currentSlideElab = index;
this.updateSliderElab();
},
updateSliderElab() {
const wrapper = $('#elab-slider-wrapper');
const dots = document.querySelectorAll('#elab-dots .slider-dot');
if (!wrapper) return;
const item = wrapper.querySelector('.schedule-item');
if (!item) return;
const itemWidth = item.offsetWidth;
const gap = 15;
const offset = this.currentSlideElab * (itemWidth + gap);
wrapper.style.transform = `translateX(-${offset}px)`;
dots.forEach((dot, i) => {
const isVisible = i >= this.currentSlideElab && i < this.currentSlideElab + 3;
dot.classList.toggle('active', isVisible);
});
},
startAutoPlayElab() {
this.stopAutoPlayElab();
this.autoPlayIntervalElab = setInterval(() => {
this.goToSlideElab(this.currentSlideElab + 1);
}, this.autoPlayDelayElab);
},
stopAutoPlayElab() {
if (this.autoPlayIntervalElab) {
clearInterval(this.autoPlayIntervalElab);
this.autoPlayIntervalElab = null;
}
},
resetAutoPlayElab() {
this.stopAutoPlayElab();
this.startAutoPlayElab();
},
async loadFromStreamers() {
try {
// Buscar locutores para montar programacao baseada em schedule_items
const res = await fetchWithRetry(`${CONFIG.apiBase}/app_locutores.php?hash=${CONFIG.hash}`);
const data = await res.json();
const items = data.data || [];
const tzTime = this.getTimeInTimezone();
const hoje = tzTime.dayOfWeek;
const minNow = tzTime.totalMinutes;
const programas = [];
items.forEach(locutor => {
if (!locutor.schedule || locutor.schedule.length === 0) return;
locutor.schedule.forEach(sched => {
// Verificar se é para hoje (no fuso da rádio)
if (sched.days && sched.days.length > 0 && !sched.days.includes(hoje)) return;
const start = sched.start_time || '00:00';
const end = sched.end_time || '23:59';
const [h1, m1] = start.split(':').map(Number);
const [h2, m2] = end.split(':').map(Number);
const startMin = h1 * 60 + m1;
let endMin = h2 * 60 + m2;
if (endMin === 0 || endMin <= startMin) endMin = 1440;
const isNow = minNow >= startMin && minNow < endMin;
programas.push({
name: locutor.display_name || 'Locutor',
start_time: start,
end_time: end,
is_now: isNow,
start_timestamp: startMin,
end_timestamp: endMin,
art: locutor.art || '',
description: locutor.comments || ''
});
});
});
if (programas.length > 0) {
// Ordenar por hora
programas.sort((a, b) => a.start_timestamp - b.start_timestamp);
this.render(programas);
} else {
this.renderEmpty();
}
} catch (e) {
this.renderEmpty();
}
},
allItems: [],
render(items) {
const grid = $('#schedule-grid');
if (!grid) return;
this.allItems = items;
this.totalItems = items.length;
const agora = Math.floor(Date.now() / 1000);
// Se tiver 3 ou menos, mostrar grid normal
if (items.length <= 3) {
grid.className = 'schedule-grid';
grid.innerHTML = items.map(prog => this.renderItem(prog, agora)).join('');
return;
}
// Se tiver mais de 3, criar slider (navega 1 por vez)
grid.className = 'schedule-grid schedule-slider';
// Encontrar o indice do programa atual (Agora)
let nowIndex = 0;
items.forEach((prog, index) => {
const isNow = prog.is_now || (agora >= prog.start_timestamp && agora < prog.end_timestamp);
if (isNow) {
nowIndex = index;
}
});
// Total de posicoes possiveis (para nao passar do fim)
const maxSlide = Math.max(0, items.length - 3);
const startSlide = Math.min(nowIndex, maxSlide);
grid.innerHTML = `
${items.map(prog => this.renderItem(prog, agora)).join('')}
${Array.from({length: items.length}, (_, i) =>
` `
).join('')}
`;
this.currentSlide = startSlide;
this.maxSlide = maxSlide;
this.initSlider();
this.startAutoPlay();
},
renderItem(prog, agora) {
const isNow = prog.is_now || (agora >= prog.start_timestamp && agora < prog.end_timestamp);
const startTime = prog.start_time || this.formatTime(prog.start_timestamp);
const endTime = prog.end_time || this.formatTime(prog.end_timestamp);
const nowClass = isNow ? 'schedule-item--now' : '';
const artUrl = prog.art || '';
const artImg = artUrl ? ` ` : '';
return `
${utils.escape(prog.name || prog.title || 'Programa')}
${utils.escape(prog.description || '')}
`;
},
initSlider() {
const prevBtn = $('#schedule-prev');
const nextBtn = $('#schedule-next');
const dots = document.querySelectorAll('#schedule-dots .slider-dot');
const container = $('#schedule-grid');
prevBtn?.addEventListener('click', () => {
this.goToSlide(this.currentSlide - 1);
this.resetAutoPlay();
});
nextBtn?.addEventListener('click', () => {
this.goToSlide(this.currentSlide + 1);
this.resetAutoPlay();
});
dots.forEach(dot => {
dot.addEventListener('click', () => {
this.goToSlide(parseInt(dot.dataset.slide));
this.resetAutoPlay();
});
});
// Pausar auto-play ao passar o mouse
container?.addEventListener('mouseenter', () => this.stopAutoPlay());
container?.addEventListener('mouseleave', () => this.startAutoPlay());
this.updateSlider();
},
goToSlide(index) {
// Loop infinito
if (index < 0) index = this.maxSlide;
if (index > this.maxSlide) index = 0;
this.currentSlide = index;
this.updateSlider();
},
updateSlider() {
const wrapper = $('#schedule-slider-wrapper');
const dots = document.querySelectorAll('#schedule-dots .slider-dot');
const prevBtn = $('#schedule-prev');
const nextBtn = $('#schedule-next');
if (!wrapper) return;
// Calcular offset baseado no tamanho do item + gap (1 item por vez)
const item = wrapper.querySelector('.schedule-item');
if (!item) return;
const itemWidth = item.offsetWidth;
const gap = 15;
const offset = this.currentSlide * (itemWidth + gap);
wrapper.style.transform = `translateX(-${offset}px)`;
// Atualizar dots - destacar os 3 visiveis
dots.forEach((dot, i) => {
const isVisible = i >= this.currentSlide && i < this.currentSlide + 3;
dot.classList.toggle('active', isVisible);
});
},
startAutoPlay() {
this.stopAutoPlay();
this.autoPlayInterval = setInterval(() => {
this.goToSlide(this.currentSlide + 1);
}, this.autoPlayDelay);
},
stopAutoPlay() {
if (this.autoPlayInterval) {
clearInterval(this.autoPlayInterval);
this.autoPlayInterval = null;
}
},
resetAutoPlay() {
this.stopAutoPlay();
this.startAutoPlay();
},
formatTime(ts) {
if (!ts) return '';
return new Date(ts * 1000).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
},
renderEmpty() {
const grid = $('#schedule-grid');
if (!grid) return;
grid.innerHTML = `
Nenhuma programação disponível
`;
}
};
// Chat
const chat = {
userName: '',
lastMessageId: 0,
refreshInterval: null,
init() {
if (!$('#chat-messages')) return;
// Verificar se ja tem nome salvo
const savedName = localStorage.getItem('chat_name_' + CONFIG.hash);
if (savedName) {
this.userName = savedName;
this.enterChat();
}
// Botao entrar no chat
$('#chat-enter')?.addEventListener('click', () => this.setUserName());
$('#chat-name')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.setUserName();
});
// Botao enviar mensagem
$('#chat-send')?.addEventListener('click', () => this.send());
$('#chat-message')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.send();
}
});
// Botao trocar nome
$('#chat-change-name')?.addEventListener('click', () => this.changeName());
},
setUserName() {
const nameInput = $('#chat-name');
const name = nameInput?.value.trim();
if (!name || name.length < 2) {
utils.toast('Digite um nome válido (mínimo 2 caracteres)', 'error');
nameInput?.focus();
return;
}
this.userName = name;
localStorage.setItem('chat_name_' + CONFIG.hash, name);
this.enterChat();
},
enterChat() {
// Esconder modal e mostrar chat
const modal = $('#chat-name-modal');
const container = $('#chat-container');
const userDisplay = $('#chat-user-display');
if (modal) modal.style.display = 'none';
if (container) container.style.display = 'flex';
if (userDisplay) userDisplay.textContent = this.userName;
// Carregar mensagens
this.load();
// Iniciar refresh automatico
if (this.refreshInterval) clearInterval(this.refreshInterval);
this.refreshInterval = setInterval(() => this.load(), CONFIG.chatInterval);
// Focar no campo de mensagem
setTimeout(() => $('#chat-message')?.focus(), 100);
},
changeName() {
// Voltar para o modal
const modal = $('#chat-name-modal');
const container = $('#chat-container');
const nameInput = $('#chat-name');
if (container) container.style.display = 'none';
if (modal) modal.style.display = 'block';
if (nameInput) {
nameInput.value = this.userName;
nameInput.focus();
nameInput.select();
}
// Parar refresh
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
},
lastMessagesHash: '',
async load() {
try {
const url = `${CONFIG.apiBase}/chat_messages.php?hash=${CONFIG.hash}`;
const res = await fetch(url);
const data = await res.json();
if (data.success && Array.isArray(data.messages)) {
// Só atualizar se houver mudanças
const newHash = JSON.stringify(data.messages);
if (newHash !== this.lastMessagesHash) {
this.lastMessagesHash = newHash;
this.render(data.messages);
}
}
} catch (e) {
console.error('[Chat] Erro:', e);
// Se nao tem mensagens carregadas, mostrar aviso
if (!this.lastMessagesHash) {
const container = $('#chat-messages');
if (container && container.children.length === 0) {
container.innerHTML = `
Erro ao conectar no chat. Tentando novamente...
`;
}
}
}
},
render(msgs) {
const container = $('#chat-messages');
if (!container) return;
if (msgs.length === 0) {
container.innerHTML = `
Nenhuma mensagem ainda. Seja o primeiro a enviar!
`;
return;
}
container.innerHTML = msgs.map(m => {
const isSent = m.nome === this.userName;
const msgClass = isSent ? 'chat__message--sent' : 'chat__message--received';
return `
${utils.escape(m.nome || 'Anonimo')}
${utils.escape(m.mensagem || '')}
${m.hora || ''}
`}).join('');
// Auto-scroll para baixo
container.scrollTop = container.scrollHeight;
},
async send() {
const msgInput = $('#chat-message');
const msg = msgInput?.value.trim();
if (!this.userName) {
utils.toast('Entre no chat primeiro', 'error');
return;
}
if (!msg) {
utils.toast('Digite uma mensagem', 'error');
msgInput?.focus();
return;
}
// Desabilitar botao enquanto envia
const sendBtn = $('#chat-send');
if (sendBtn) sendBtn.disabled = true;
try {
const form = new FormData();
form.append('hash', CONFIG.hash);
form.append('nome', this.userName);
form.append('mensagem', msg);
const res = await fetch(`${CONFIG.apiBase}/send_chat_message.php`, { method: 'POST', body: form });
const data = await res.json();
if (data.success) {
msgInput.value = '';
this.load(); // Recarregar mensagens
} else {
utils.toast(data.message || 'Erro ao enviar', 'error');
}
} catch (e) {
utils.toast('Erro ao enviar mensagem', 'error');
} finally {
if (sendBtn) sendBtn.disabled = false;
msgInput?.focus();
}
}
};
// UI
const ui = {
init() {
// Mobile nav
const navToggle = $('#nav-toggle');
const navMenu = $('#nav-menu');
const navBackdrop = $('#nav-backdrop');
const closeMobileNav = () => {
if (!navMenu) return;
navMenu.classList.remove('active');
navToggle?.classList.remove('active');
navBackdrop?.classList.remove('active');
document.body.classList.remove('menu-open');
navToggle?.setAttribute('aria-expanded', 'false');
};
const openMobileNav = () => {
if (!navMenu) return;
navMenu.classList.add('active');
navToggle?.classList.add('active');
navBackdrop?.classList.add('active');
document.body.classList.add('menu-open');
navToggle?.setAttribute('aria-expanded', 'true');
};
if (navToggle) {
navToggle.onclick = () => {
if (navMenu.classList.contains('active')) closeMobileNav();
else openMobileNav();
};
}
if (navBackdrop) navBackdrop.onclick = closeMobileNav;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && navMenu?.classList.contains('active')) closeMobileNav();
});
// Smooth scroll
$$('a[href^="#"]').forEach(a => {
a.onclick = (e) => {
e.preventDefault();
const target = $(a.getAttribute('href'));
if (target) target.scrollIntoView({ behavior: 'smooth' });
closeMobileNav();
};
});
// Action buttons
$('#btn-share').onclick = () => this.share();
$('#btn-recado').onclick = () => {
const widget = document.getElementById('recados-unified-widget');
if (widget) {
widget.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight the widget briefly
widget.style.transition = 'box-shadow 0.3s ease';
widget.style.boxShadow = '0 0 30px var(--dynamic-glow)';
setTimeout(() => { widget.style.boxShadow = ''; }, 2000);
}
};
$('#btn-pwa').onclick = () => this.installPWA();
$('#btn-whatsapp').onclick = () => {
if (CONFIG.whatsapp) window.open(`https://wa.me/${CONFIG.whatsapp}`, '_blank');
};
// WhatsApp Music form
const whatsappMusicForm = $('#whatsapp-music-form');
if (whatsappMusicForm) {
const whatsappFormTime = $('#whatsapp-form-time');
if (whatsappFormTime) whatsappFormTime.value = Date.now();
whatsappMusicForm.onsubmit = (e) => this.sendWhatsappMusic(e);
}
// PWA
this.setupPWA();
},
share() {
const data = { title: CONFIG.radioName, text: `Ouca ${CONFIG.radioName} ao vivo!`, url: location.href };
if (navigator.share) {
navigator.share(data);
} else {
navigator.clipboard.writeText(location.href);
utils.toast('Link copiado!', 'success');
}
},
deferredPrompt: null,
setupPWA() {
// Usa o prompt capturado no início do script
if (window.__pwaPrompt) {
this.deferredPrompt = window.__pwaPrompt;
console.log('[PWA] Prompt capturado do início do script');
}
// Continua ouvindo caso o evento ainda não tenha disparado
window.addEventListener('beforeinstallprompt', (e) => {
console.log('[PWA] beforeinstallprompt capturado em setupPWA');
this.deferredPrompt = e;
window.__pwaPrompt = e;
});
// Detectar quando foi instalado
window.addEventListener('appinstalled', () => {
this.deferredPrompt = null;
window.__pwaPrompt = null;
utils.toast('App instalado com sucesso!', 'success');
});
if ('serviceWorker' in navigator) {
// Nao registrar SW em dominios personalizados (SW deve ser same-origin)
const isCustomDomain = !window.location.hostname.includes('player-webservic.com');
if (isCustomDomain) {
console.log('[PWA] Service Worker desabilitado em dominio personalizado');
} else {
// Detectar scope correto baseado na URL atual
const pathParts = window.location.pathname.split('/');
const siteIdx = pathParts.indexOf('site1');
const scope = siteIdx >= 0 ? pathParts.slice(0, siteIdx + 1).join('/') + '/' : '/';
navigator.serviceWorker.register(
`${CONFIG.apiBase}/sw.php?h=${CONFIG.hash}`,
{ scope: scope }
).then(reg => {
console.log('[PWA] Service Worker registrado com scope:', scope);
console.log('[PWA] SW state:', reg.active ? 'active' : reg.installing ? 'installing' : 'waiting');
}).catch(err => {
console.warn('[PWA] Erro ao registrar SW:', err);
});
}
}
},
async installPWA() {
// Se já está em modo standalone (já instalado)
if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone) {
utils.toast('O app já está instalado!', 'info');
return;
}
// Tenta usar o prompt capturado globalmente
const prompt = this.deferredPrompt || window.__pwaPrompt;
if (prompt) {
try {
prompt.prompt();
const result = await prompt.userChoice;
if (result.outcome === 'accepted') {
utils.toast('App instalado com sucesso!', 'success');
}
this.deferredPrompt = null;
window.__pwaPrompt = null;
} catch (err) {
console.warn('Erro ao instalar PWA:', err);
utils.toast('Erro ao instalar. Tente pelo menu do navegador.', 'error');
}
} else {
// Detectar navegador e dar instrução específica
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isChrome = /Chrome/.test(navigator.userAgent) && !/Edge|Edg/.test(navigator.userAgent);
const isFirefox = /Firefox/.test(navigator.userAgent);
const isSamsung = /SamsungBrowser/.test(navigator.userAgent);
let msg = '';
if (isIOS) {
msg = 'Toque no botão de compartilhar e depois em "Adicionar à Tela de Início"';
} else if (isChrome) {
msg = 'Toque nos 3 pontos do menu e depois em "Instalar aplicativo"';
} else if (isSamsung) {
msg = 'Toque no menu e depois em "Adicionar página a" > "Tela inicial"';
} else if (isFirefox) {
msg = 'Toque no menu e depois em "Instalar"';
} else {
msg = 'Use o menu do navegador para adicionar à tela inicial';
}
utils.toast(msg, 'info', 5000);
}
},
async sendWhatsappMusic(e) {
e.preventDefault();
const form = e.target;
const data = new FormData(form);
// Anti-bot: verificar honeypot
const honeypot = form.querySelector('[name="email_confirm"]');
if (honeypot && honeypot.value) {
utils.toast('Número cadastrado!', 'success');
form.reset();
return;
}
// Anti-bot: verificar tempo minimo (3 segundos)
const formTime = form.querySelector('[name="form_time"]');
if (formTime && formTime.value) {
const elapsed = Date.now() - parseInt(formTime.value);
if (elapsed < 3000) {
utils.toast('Aguarde um momento...', 'info');
return;
}
}
// Validar numero
const celular = form.querySelector('[name="celular"]').value.replace(/\D/g, '');
if (celular.length < 10 || celular.length > 13) {
utils.toast('Número inválido. Use DDD + número', 'error');
return;
}
data.append('hash', CONFIG.hash);
const btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true;
try {
const res = await fetch(`${CONFIG.apiBase}/cadastro_zap.php`, { method: 'POST', body: data });
const result = await res.json();
if (result.success) {
utils.toast('Número cadastrado com sucesso!', 'success');
form.reset();
if (formTime) formTime.value = Date.now();
} else {
utils.toast(result.message || 'Erro ao cadastrar', 'error');
}
} catch (e) {
utils.toast('Erro ao cadastrar', 'error');
} finally {
if (btn) btn.disabled = false;
}
}
};
// Particles System (exposto globalmente para controle via Site Config)
window.particles = {
canvas: null,
ctx: null,
particles: [],
isPlaying: false,
animationId: null,
init() {
this.canvas = $('#particles-canvas');
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.createParticles();
this.animate();
},
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
},
createParticles() {
const count = Math.min(60, Math.floor(window.innerWidth / 25));
this.particles = [];
for (let i = 0; i < count; i++) {
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
baseY: 0,
size: Math.random() * 3 + 1,
baseSpeed: Math.random() * 0.3 + 0.1,
speed: 0,
opacity: Math.random() * 0.3 + 0.1,
baseOpacity: 0,
wobble: Math.random() * Math.PI * 2,
wobbleSpeed: Math.random() * 0.02 + 0.01
});
}
this.particles.forEach(p => {
p.baseY = p.y;
p.speed = p.baseSpeed;
p.baseOpacity = p.opacity;
});
},
setPlaying(playing) {
this.isPlaying = playing;
},
animate() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.particles.forEach(p => {
// Calculate speed and opacity based on playing state
const targetSpeed = this.isPlaying ? p.baseSpeed * (2 + Math.random() * 3) : p.baseSpeed;
const targetOpacity = this.isPlaying ? p.baseOpacity * (1.5 + Math.random()) : p.baseOpacity;
// Smooth transition
p.speed += (targetSpeed - p.speed) * 0.05;
p.opacity += (targetOpacity - p.opacity) * 0.05;
// Movement - particles rise up
p.y -= p.speed;
// Wobble effect (horizontal sway)
p.wobble += p.wobbleSpeed * (this.isPlaying ? 2 : 1);
const wobbleAmount = this.isPlaying ? Math.sin(p.wobble) * 2 : Math.sin(p.wobble) * 0.5;
p.x += wobbleAmount * 0.3;
// Reset when off screen
if (p.y < -10) {
p.y = this.canvas.height + 10;
p.x = Math.random() * this.canvas.width;
}
// Keep within horizontal bounds
if (p.x < 0) p.x = this.canvas.width;
if (p.x > this.canvas.width) p.x = 0;
// Draw particle
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
this.ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(p.opacity, 0.6)})`;
this.ctx.fill();
});
this.animationId = requestAnimationFrame(() => this.animate());
}
};
// Modal functions
window.openModal = (id) => $(`#${id}`).classList.add('active');
window.closeModal = (id) => $(`#${id}`).classList.remove('active');
// Info Modal functions
window.openInfoModal = (type) => {
const modal = $(`#info-modal-${type}`);
if (modal) {
modal.classList.add('visible');
document.body.style.overflow = 'hidden';
}
};
window.closeInfoModal = () => {
$$('.info-modal.visible').forEach(m => m.classList.remove('visible'));
document.body.style.overflow = '';
};
// Notícia modal
window.openNoticiaModal = (index) => {
const item = noticias.items?.[index];
if (!item) return;
const defaultImg = 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const img = item.thumbnail || defaultImg;
const meta = [];
if (item.author) meta.push(` ${utils.escape(item.author)} `);
if (item.pubDate) meta.push(` ${noticias.formatDate(item.pubDate)} `);
document.getElementById('noticia-modal-title').innerHTML = ` ${utils.escape(item.title)}`;
document.getElementById('noticia-modal-body').innerHTML = `
${meta.length ? `${meta.join('')}
` : ''}
${utils.escape(item.content)}
`;
document.getElementById('noticia-modal').classList.add('visible');
document.body.style.overflow = 'hidden';
};
window.closeNoticiaModal = () => {
document.getElementById('noticia-modal').classList.remove('visible');
document.body.style.overflow = '';
};
window.noticiasVerMais = () => {
if (!noticias.items) return;
const nextBatch = noticias.perPage;
const oldCount = noticias.visibleCount;
noticias.visibleCount = Math.min(oldCount + nextBatch, noticias.items.length);
// Remover botão antigo
const wrap = els.newsGrid.querySelector('.news-ver-mais-wrap');
if (wrap) wrap.remove();
// Adicionar novos cards
let html = '';
for (let i = oldCount; i < noticias.visibleCount; i++) {
html += noticias.renderCard(noticias.items[i], i);
}
// Novo botão se ainda houver mais
if (noticias.items.length > noticias.visibleCount) {
html += `
Ver mais noticias
(+${noticias.items.length - noticias.visibleCount})
`;
}
els.newsGrid.insertAdjacentHTML('beforeend', html);
};
// Blog da Rádio modal
window.openBlogModal = (index) => {
const item = blogRadio.items?.[index];
if (!item) return;
const defaultImg = 'https://app.player-webservic.com/radio-dashboard/model1/uploads/sites/09d6a53ea304e896f651612ad756400a/site_logo_1779391929.jpg';
const img = item.thumbnail || defaultImg;
const meta = [];
if (item.author) meta.push(` ${utils.escape(item.author)} `);
if (item.pubDate) meta.push(` ${blogRadio.formatDate(item.pubDate)} `);
document.getElementById('noticia-modal-title').innerHTML = ` ${utils.escape(item.title)}`;
document.getElementById('noticia-modal-body').innerHTML = `
${meta.length ? `${meta.join('')}
` : ''}
${utils.escape(item.content)}
`;
document.getElementById('noticia-modal').classList.add('visible');
document.body.style.overflow = 'hidden';
};
window.blogVerMais = async () => {
if (!blogRadio.hasMore) return;
const grid = document.getElementById('blog-grid');
if (!grid) return;
// Loading state no botao (anti double-click)
const btnWrap = grid.querySelector('div[style*="text-align:center"]');
const btn = btnWrap ? btnWrap.querySelector('.news-ver-mais-btn') : null;
if (btn) { btn.disabled = true; btn.style.opacity = '0.6'; }
try {
const res = await fetchWithRetry(`${CONFIG.apiBase}/get_blog_posts.php?hash=${CONFIG.hash}&limit=${blogRadio.perPage}&offset=${blogRadio.offset}`);
const data = await res.json();
if (!data.success || !data.items?.length) {
blogRadio.hasMore = false;
if (btnWrap) btnWrap.remove();
return;
}
const startIndex = blogRadio.items.length;
blogRadio.items = blogRadio.items.concat(data.items);
blogRadio.offset += data.items.length;
blogRadio.visibleCount = blogRadio.items.length;
blogRadio.hasMore = !!data.has_more;
if (typeof data.total === 'number') blogRadio.total = data.total;
// Encontrar ou criar a row de cards
let row = grid.querySelector('.blog-cards-row');
if (!row) {
row = document.createElement('div');
row.className = 'blog-cards-row';
if (btnWrap) grid.insertBefore(row, btnWrap);
else grid.appendChild(row);
}
let html = '';
for (let i = 0; i < data.items.length; i++) {
html += blogRadio.renderCard(data.items[i], startIndex + i, false);
}
row.insertAdjacentHTML('beforeend', html);
// Reposicionar botao
if (btnWrap) btnWrap.remove();
if (blogRadio.hasMore) {
const restante = Math.max(0, blogRadio.total - blogRadio.items.length);
grid.insertAdjacentHTML('beforeend', `
Ver mais posts
(+${restante})
`);
}
} catch(e) {
if (btn) { btn.disabled = false; btn.style.opacity = ''; }
}
};
// Close info modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeInfoModal();
closeNoticiaModal();
}
});
// Close modal on backdrop click
$$('.modal').forEach(modal => {
modal.onclick = (e) => {
if (e.target === modal) modal.classList.remove('active');
};
});
// Weather Widget
const weather = {
cidade: '',
apiKey: '',
async load() {
const container = $('#weather-content');
if (!container || !this.cidade || !this.apiKey) return;
try {
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(this.cidade)}&appid=${this.apiKey}&units=metric&lang=pt_br`;
const response = await fetch(url);
const data = await response.json();
if (data.cod !== 200) {
container.innerHTML = `
${data.message || 'Erro ao carregar clima'}
`;
return;
}
const temp = Math.round(data.main.temp);
const feelsLike = Math.round(data.main.feels_like);
const humidity = data.main.humidity;
const wind = Math.round(data.wind.speed * 3.6); // m/s para km/h
const description = data.weather[0].description;
const icon = data.weather[0].icon;
const cityName = data.name;
container.innerHTML = `
${description}
${cityName}
${feelsLike}°C
Sensacao
${humidity}%
Umidade
${wind} km/h
Vento
`;
// Atualizar a cada 10 minutos
setTimeout(() => this.load(), 600000);
} catch (error) {
console.error('Erro ao carregar clima:', error);
container.innerHTML = `
Erro ao carregar clima
`;
}
}
};
// Promoções Widget
const promocoes = {
currentIndex: 0,
data: [],
autoPlayInterval: null,
async load() {
const widget = $('#promocoes-widget');
const container = $('#promocoes-content');
if (!container || !widget) {
console.log('[Promocoes] Widget ou container não encontrado');
return;
}
// Verificar se o módulo está desativado na configuração
const checkModuleEnabled = () => {
if (window.siteConfig && window.siteConfig.modulos && window.siteConfig.modulos.promocoes) {
const ativoValue = window.siteConfig.modulos.promocoes.ativo;
const isDesativado = ativoValue === false || ativoValue === 0 || ativoValue === "false" || ativoValue === "0";
if (isDesativado) {
widget.style.display = 'none';
const navLink = $('#nav-promocoes');
const footerLink = $('#footer-promocoes');
if (navLink) navLink.style.display = 'none';
if (footerLink) footerLink.style.display = 'none';
console.log('[Promocoes] Módulo desativado na configuração');
return false;
}
}
return true;
};
// Se config ainda não carregou, aguardar
if (!window.siteConfigLoaded) {
await new Promise(resolve => {
const check = () => {
if (window.siteConfigLoaded) resolve();
else setTimeout(check, 100);
};
check();
});
}
// Verificar se módulo está habilitado
if (!checkModuleEnabled()) {
return;
}
console.log('[Promocoes] Carregando promoções...');
try {
const response = await fetch(`${CONFIG.apiBase}/promocoes.php?hash=${CONFIG.hash}&action=list`);
const result = await response.json();
console.log('[Promocoes] Resposta:', result);
if (result.success && result.promocoes && result.promocoes.length > 0) {
this.data = result.promocoes;
widget.style.display = '';
this.render();
// Mostrar links no menu
const navLink = $('#nav-promocoes');
const footerLink = $('#footer-promocoes');
if (navLink) navLink.style.display = '';
if (footerLink) footerLink.style.display = '';
if (window.adjustNavOverflow) window.adjustNavOverflow();
console.log('[Promocoes] Widget exibido com', result.promocoes.length, 'promoções');
} else {
widget.style.display = 'none';
// Esconder links do menu
const navLink = $('#nav-promocoes');
const footerLink = $('#footer-promocoes');
if (navLink) navLink.style.display = 'none';
if (footerLink) footerLink.style.display = 'none';
console.log('[Promocoes] Nenhuma promoção ativa');
}
} catch (error) {
console.error('[Promocoes] Erro ao carregar:', error);
widget.style.display = 'none';
}
},
render() {
const container = $('#promocoes-content');
if (!container || this.data.length === 0) return;
const isSlider = this.data.length > 1;
let html = '';
if (isSlider) {
html = `
${this.data.map((_, i) => ` `).join('')}
`;
} else {
html = this.renderCard(this.data[0], 0);
}
container.innerHTML = html;
if (isSlider) {
this.initSlider();
}
this.bindFormEvents();
},
renderCard(promo, index) {
const hasImage = promo.imagem && promo.imagem.length > 0;
const hasGanhadores = promo.ganhadores && promo.ganhadores.length > 0;
const isEncerrada = promo.encerrada === true;
let ganhadoresHtml = '';
if (hasGanhadores) {
ganhadoresHtml = `
`;
}
// Se encerrada, mostra aviso em vez do formulário
let formHtml = '';
if (isEncerrada) {
formHtml = `
Promoção Encerrada
${promo.data_fim_formatada ? `Finalizada em ${promo.data_fim_formatada} ` : ''}
`;
} else {
formHtml = `
`;
}
return `
`;
},
initSlider() {
const nav = $('#promocoes-nav');
if (!nav) return;
nav.querySelectorAll('.promocoes-nav__dot').forEach(dot => {
dot.addEventListener('click', () => {
this.goTo(parseInt(dot.dataset.index));
});
});
// Auto-play
this.startAutoPlay();
},
goTo(index) {
if (index < 0 || index >= this.data.length) return;
this.currentIndex = index;
const wrapper = $('#promocoes-wrapper');
if (wrapper) {
wrapper.style.transform = `translateX(-${index * 100}%)`;
}
const nav = $('#promocoes-nav');
if (nav) {
nav.querySelectorAll('.promocoes-nav__dot').forEach((dot, i) => {
dot.classList.toggle('active', i === index);
});
}
},
startAutoPlay() {
if (this.autoPlayInterval) clearInterval(this.autoPlayInterval);
this.autoPlayInterval = setInterval(() => {
const next = (this.currentIndex + 1) % this.data.length;
this.goTo(next);
}, 5000);
},
bindFormEvents() {
$$('.promocao-card__form').forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const promoId = form.dataset.promocaoId;
const nome = form.querySelector('[name="nome"]').value.trim();
const email = form.querySelector('[name="email"]').value.trim();
const telefone = form.querySelector('[name="telefone"]').value.trim();
const btn = form.querySelector('button[type="submit"]');
if (!nome || !email) {
utils.toast('Preencha nome e email', 'error');
return;
}
btn.disabled = true;
btn.innerHTML = ' Enviando...';
try {
const formData = new FormData();
formData.append('hash', CONFIG.hash);
formData.append('action', 'inscrever');
formData.append('id_promocao', promoId);
formData.append('nome', nome);
formData.append('email', email);
formData.append('telefone', telefone);
const response = await fetch(`${CONFIG.apiBase}/promocoes.php`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
form.innerHTML = `
`;
utils.toast('Você está participando!', 'success');
} else {
utils.toast(result.message || 'Erro ao inscrever', 'error');
btn.disabled = false;
btn.innerHTML = ' Participar';
}
} catch (error) {
utils.toast('Erro de conexão', 'error');
btn.disabled = false;
btn.innerHTML = ' Participar';
}
});
});
}
};
// Horóscopo Widget
const horoscopo = {
signos: [
{ id: 'aries', nome: 'Áries', icon: '♈︎', periodo: '21/03 - 19/04' },
{ id: 'touro', nome: 'Touro', icon: '♉︎', periodo: '20/04 - 20/05' },
{ id: 'gemeos', nome: 'Gêmeos', icon: '♊︎', periodo: '21/05 - 20/06' },
{ id: 'cancer', nome: 'Câncer', icon: '♋︎', periodo: '21/06 - 22/07' },
{ id: 'leao', nome: 'Leão', icon: '♌︎', periodo: '23/07 - 22/08' },
{ id: 'virgem', nome: 'Virgem', icon: '♍︎', periodo: '23/08 - 22/09' },
{ id: 'libra', nome: 'Libra', icon: '♎︎', periodo: '23/09 - 22/10' },
{ id: 'escorpiao', nome: 'Escorpião', icon: '♏︎', periodo: '23/10 - 21/11' },
{ id: 'sagitario', nome: 'Sagitário', icon: '♐︎', periodo: '22/11 - 21/12' },
{ id: 'capricornio', nome: 'Capricórnio', icon: '♑︎', periodo: '22/12 - 19/01' },
{ id: 'aquario', nome: 'Aquário', icon: '♒︎', periodo: '20/01 - 18/02' },
{ id: 'peixes', nome: 'Peixes', icon: '♓︎', periodo: '19/02 - 20/03' }
],
previsoes: {
amor: [
'Momento propício para declarações. Abra seu coração.',
'Surpresas agradáveis no campo amoroso. Esteja receptivo.',
'Fortaleça os laços com quem você ama. Diálogo é a chave.',
'Novos encontros podem surgir. Mantenha-se aberto.',
'Fase de harmonia nos relacionamentos. Aproveite.',
'Pequenos gestos farão grande diferença. Seja carinhoso.',
'Reflexão sobre seus sentimentos trará clareza.',
'Momento de cumplicidade. Valorize seu parceiro.'
],
trabalho: [
'Oportunidades de crescimento profissional surgirão.',
'Concentre-se nas tarefas importantes. Foco é essencial.',
'Bom momento para iniciar novos projetos.',
'Reconhecimento pelo seu esforço está próximo.',
'Colaboração em equipe trará resultados positivos.',
'Criatividade em alta. Aproveite para inovar.',
'Organize suas prioridades para maior produtividade.',
'Networking pode abrir portas importantes.'
],
saude: [
'Cuide da alimentação e hidratação. Seu corpo agradece.',
'Atividade física trará mais disposição. Mexa-se!',
'Momento para descansar e recarregar as energias.',
'Atenção ao estresse. Pratique momentos de relaxamento.',
'Energia positiva em alta. Aproveite a vitalidade.',
'Equilibre trabalho e lazer para bem-estar.',
'Ouça os sinais do seu corpo. Descanse quando preciso.',
'Bom momento para iniciar hábitos saudáveis.'
],
dinheiro: [
'Evite gastos impulsivos. Planejamento é fundamental.',
'Oportunidade financeira pode surgir. Fique atento.',
'Momento favorável para investimentos seguros.',
'Revise seu orçamento e corte excessos.',
'Entrada de dinheiro inesperada possível.',
'Cautela nas decisões financeiras hoje.',
'Bom momento para quitar dívidas pendentes.',
'Prosperidade à vista com planejamento adequado.'
]
},
cores: ['Vermelho', 'Azul', 'Verde', 'Amarelo', 'Laranja', 'Roxo', 'Rosa', 'Branco', 'Dourado', 'Prata'],
numeros: ['3', '7', '9', '12', '21', '33', '42', '58', '69', '77'],
init() {
const widget = $('#horoscopo-widget');
const container = $('#horoscopo-signos');
if (!widget || !container) return;
// Verificar se o módulo está ativo na configuração
// (A visibilidade é controlada pelo applyModuleVisibility, mas inicializamos os eventos)
// Renderizar signos
container.innerHTML = this.signos.map(s => `
${s.icon}
${s.nome}
`).join('');
// Eventos
container.querySelectorAll('.signo-btn').forEach(btn => {
btn.addEventListener('click', () => {
const signoId = btn.dataset.signo;
this.mostrarPrevisao(signoId);
container.querySelectorAll('.signo-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Verificar configuração do módulo e mostrar se ativo
// Usa window.siteConfig que é global
const checkVisibility = () => {
// Aguardar siteConfig carregar
if (!window.siteConfigLoaded) {
setTimeout(checkVisibility, 200);
return;
}
let isAtivo = true; // Ativo por padrão
if (window.siteConfig && window.siteConfig.modulos && window.siteConfig.modulos.horoscopo) {
// Verificar se está explicitamente desativado (false, 0, "false", "0")
const ativoValue = window.siteConfig.modulos.horoscopo.ativo;
isAtivo = ativoValue !== false && ativoValue !== 0 && ativoValue !== "false" && ativoValue !== "0" && ativoValue !== null;
}
if (isAtivo) {
widget.style.display = '';
// Mostrar links no menu
const navLink = $('#nav-horoscopo');
const footerLink = $('#footer-horoscopo');
if (navLink) navLink.style.display = '';
if (footerLink) footerLink.style.display = '';
if (window.adjustNavOverflow) window.adjustNavOverflow();
} else {
// Garantir que fica escondido se desativado
widget.style.display = 'none';
const navLink = $('#nav-horoscopo');
const footerLink = $('#footer-horoscopo');
if (navLink) navLink.style.display = 'none';
if (footerLink) footerLink.style.display = 'none';
}
};
setTimeout(checkVisibility, 100); // Iniciar verificação
},
mostrarPrevisao(signoId) {
const signo = this.signos.find(s => s.id === signoId);
if (!signo) return;
const previsaoEl = $('#horoscopo-previsao');
const signosEl = $('#horoscopo-signos');
// Gerar previsão baseada na data (consistente durante o dia)
const hoje = new Date();
const seed = hoje.getFullYear() * 10000 + (hoje.getMonth() + 1) * 100 + hoje.getDate();
const signoIndex = this.signos.findIndex(s => s.id === signoId);
const getIndex = (arr, offset) => {
return Math.abs((seed + signoIndex + offset) % arr.length);
};
const previsao = {
amor: this.previsoes.amor[getIndex(this.previsoes.amor, 1)],
trabalho: this.previsoes.trabalho[getIndex(this.previsoes.trabalho, 2)],
saude: this.previsoes.saude[getIndex(this.previsoes.saude, 3)],
cor: this.cores[getIndex(this.cores, 4)],
numero: this.numeros[getIndex(this.numeros, 5)]
};
previsaoEl.innerHTML = `
❤️ Amor: ${previsao.amor}
💼 Trabalho: ${previsao.trabalho}
🏃 Saúde: ${previsao.saude}
Cor do dia
${previsao.cor}
Número
${previsao.numero}
Elemento
${this.getElemento(signoId)}
Ver outros signos
`;
signosEl.style.display = 'none';
previsaoEl.style.display = 'block';
// Bind evento do botão voltar
const btnVoltar = document.getElementById('btn-voltar-signos');
if (btnVoltar) {
btnVoltar.addEventListener('click', () => this.voltarSignos());
}
},
voltarSignos() {
const previsaoEl = $('#horoscopo-previsao');
const signosEl = $('#horoscopo-signos');
signosEl.style.display = '';
previsaoEl.style.display = 'none';
signosEl.querySelectorAll('.signo-btn').forEach(b => b.classList.remove('active'));
},
getElemento(signoId) {
const elementos = {
aries: 'Fogo', leao: 'Fogo', sagitario: 'Fogo',
touro: 'Terra', virgem: 'Terra', capricornio: 'Terra',
gemeos: 'Ar', libra: 'Ar', aquario: 'Ar',
cancer: 'Água', escorpiao: 'Água', peixes: 'Água'
};
return elementos[signoId] || 'Fogo';
}
};
// Delegated listeners - Top Musicas + Top Videos votação + vídeo
document.addEventListener('click', (e) => {
const voteBtn = e.target.closest('.top-vote-btn');
if (voteBtn) {
e.preventDefault();
const videoId = voteBtn.dataset.videoId;
const musicId = voteBtn.dataset.musicId;
const voteType = voteBtn.dataset.vote;
if (videoId && voteType) {
topVideos.vote(videoId, voteType);
} else if (musicId && voteType) {
topMusicas.vote(musicId, voteType);
}
}
const playBtn = e.target.closest('.top-play-btn, .top-video-play-btn');
if (playBtn) {
e.preventDefault();
const videoUrl = playBtn.dataset.video;
if (videoUrl) openVideoModal(videoUrl);
}
// Thumb click — abrir imagem em lightbox
const thumbCard = e.target.closest('.top-video-card__thumb');
if (thumbCard) {
e.preventDefault();
const src = thumbCard.dataset.thumbSrc;
const title = thumbCard.dataset.thumbTitle;
if (src) openThumbLightbox(src, title);
}
// Video PiP toggle
if (e.target.closest('#video-pip-toggle')) {
e.preventDefault();
toggleVideoPip();
return;
}
// Video modal close
if (e.target.closest('.video-modal-close')) {
closeVideoModal();
return;
}
// Click no backdrop (modal full — não no pip)
const videoModal = document.getElementById('video-modal-overlay');
if (e.target === videoModal && !videoModal.classList.contains('pip')) {
closeVideoModal();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeVideoModal();
});
// Auto-minimize video on scroll
window.addEventListener('scroll', () => {
const modal = document.getElementById('video-modal-overlay');
if (!modal || !modal.classList.contains('active')) return;
const scrollDelta = Math.abs(window.scrollY - videoModalScrollY);
if (scrollDelta > 150 && !videoModalIsPip) {
toggleVideoPip();
}
}, { passive: true });
// Album Fotos delegation
document.addEventListener('click', (e) => {
const tab = e.target.closest('.album-fotos-tab');
if (tab) {
albumFotos.renderAlbum(parseInt(tab.dataset.albumIdx));
}
const foto = e.target.closest('.album-foto-item');
if (foto) {
const ai = parseInt(foto.dataset.albumIdx);
const fi = parseInt(foto.dataset.fotoIdx);
if (albumFotos.albums[ai]) {
albumFotos.openLightbox(albumFotos.albums[ai].fotos, fi);
}
}
if (e.target.closest('.album-fotos-ver-mais')) {
albumFotos.showMore();
}
});
// Init
document.addEventListener('DOMContentLoaded', () => {
// Garante contraste do accent inicial (cor fixa do painel passa
// pelo guard antes da extracao da capa rodar).
(function () {
const v = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim();
let rgb = null, m;
if ((m = v.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/))) {
rgb = [+m[1], +m[2], +m[3]];
} else if ((m = v.match(/^#([0-9a-f]{3,8})$/i))) {
const h = m[1].length === 3 ? m[1].split('').map(c => c + c).join('') : m[1];
rgb = [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16)];
}
if (rgb && !rgb.some(isNaN)) window.applyDynamicAccent(rgb[0], rgb[1], rgb[2]);
})();
loadFeatures();
particles.init();
player.init();
history.load();
locutores.load();
topMusicas.load();
topVideos.load();
albumFotos.load();
programacao.load();
pedidos.init();
noticias.load();
blogRadio.load();
chat.init();
weather.load();
promocoes.load();
horoscopo.init();
ui.init();
});
})();
Sua privacidade é importante
Este site utiliza cookies e outras tecnologias para melhorar sua experiência e nos ajudar a entender seu uso.
Ao continuar navegando, você aceita nossas políticas de privacidade e cookies .
Saiba mais
Aceitar
Sobre esta Política
A Web Rádio Old Music Six Seven está comprometida em proteger sua privacidade. Esta política descreve como coletamos,
usamos e protegemos suas informações quando você utiliza nosso site e aplicativos.
O que são Cookies?
Cookies são pequenos arquivos de texto armazenados em seu dispositivo quando você visita nosso site.
Eles nos ajudam a lembrar suas preferências e melhorar sua experiência de navegação.
Cookies que Utilizamos
Cookies Essenciais: Necessários para o funcionamento básico do site, como manter sua sessão ativa e preferências de volume.
Cookies de Desempenho: Coletam informações sobre como você usa o site para nos ajudar a melhorar.
Cookies de Funcionalidade: Lembram suas escolhas (como nome no chat) para personalizar sua experiência.
Cookies de Mídia: Permitem o funcionamento do player de áudio e streaming.
Informações Coletadas
Podemos coletar as seguintes informações:
Dados de uso do site (páginas visitadas, tempo de permanência)
Informações do dispositivo (tipo de navegador, sistema operacional)
Nome de usuário fornecido voluntariamente no chat
Preferências de áudio e qualidade de streaming
Seus Direitos
Você tem o direito de:
Acessar, corrigir ou excluir suas informações pessoais
Desativar cookies através das configurações do seu navegador
Solicitar informações sobre como seus dados são utilizados
Retirar seu consentimento a qualquer momento
Contato
Se você tiver dúvidas sobre esta política ou sobre como tratamos seus dados,
entre em contato conosco através das nossas redes sociais ou canais de comunicação disponíveis no site.
Atualizações
Esta política pode ser atualizada periodicamente. Recomendamos que você a revise regularmente.
A data da última atualização está indicada abaixo.
Última atualização: 25/05/2026